SearchValues<T> अंततः .NET 10 में बड़े बफ़र्स (buffers) में विरल डेलीमिटर्स (sparse delimiters) को स्कैन करते समय एक मापने योग्य बढ़त दिखाता है; 32 MB पेलोड पर SearchValues 3.3 ms में पूरा हो गया, जबकि IndexOfAny को 5.6 ms लगे।

SearchValues क्यों आया

.NET 8 ने SearchValues<T> को कैरेक्टर्स (या बाइट्स) के एक सेट को प्री-कंप्यूट करने और रनटाइम को उस सेट के लिए सबसे तेज़ स्कैनिंग एल्गोरिदम चुनने देने के लिए पेश किया था। ऑब्जेक्ट को एक बार बनाएं, और फिर जब भी आपको किसी span में किसी भी वैल्यू को ढूंढना हो, तो उसे दोबारा इस्तेमाल करें। रनटाइम वेक्टरकृत निर्देशों (vectorized instructions), ब्रांच-फ्री लूप्स (branch-free loops), या अन्य लो-लेवल ट्रिक्स का उपयोग कर सकता है जिन्हें हाथ से लिखे गए विकल्प बहुत अधिक प्रयास के बिना नहीं छू सकते।

महत्वपूर्ण बेंचमार्क

जिन परीक्षणों ने इस चर्चा को जन्म दिया, उनमें पांच डेलीमिटर कैरेक्टर्स वाले 32 MB के बफ़र का उपयोग किया गया था। .NET 10 पर तीन दृष्टिकोणों का समय मापा गया:

  • IndexOfAny(char[]) – 10.2 ms
  • Span पर मैन्युअल foreach लूप – 19.4 ms
  • SearchValues – 9.7 ms

SearchValues ने बिल्ट-इन IndexOfAny को केवल आधे मिलीसेकंड से पीछे छोड़ा। यह परिणाम कोई नाटकीय "पांच गुना तेज़" होने का दावा नहीं है जैसा कि फ़ोरम पर घूमता रहता है; आधुनिक .NET पहले से ही छोटे सेट्स के लिए IndexOfAny को ऑप्टिमाइज़ करता है, जिससे अंतर कम हो जाता है।

जब डेलीमिटर्स दुर्लभ होते हैं—हर 60 बाइट्स के बजाय हर 40 KB में एक बार दिखाई देते हैं—तो तस्वीर बदल जाती है:

  • IndexOfAny(char[]) – 5.6 ms
  • SearchValues – 3.3 ms

अब SearchValues 1.7 × तेज़ है क्योंकि इंजन वेक्टरकृत स्टेप्स (vectorized steps) के साथ नॉन-मैचिंग डेटा के लंबे हिस्सों से तेज़ी से गुजरता है, जबकि IndexOfAny कम आक्रामक (less aggressive) पथ पर वापस चला जाता है।

छिपा हुआ खर्च

SearchValues मुफ्त नहीं है। ऑब्जेक्ट बनाने से मेमोरी एलोकेट होती है और इंटरनल लुकअप टेबल बनती हैं। यदि आप इसे हर मेथड कॉल पर इंस्टेंटिएट (instantiate) करते हैं, तो इसका ओवरहेड रनटाइम के किसी भी लाभ को खत्म कर देगा। एक बेंचमार्क, जिसने छोटी लाइनों पर स्कैनिंग रूटीन को दस लाख बार कॉल किया, उसने दिखाया:

  • Static (reused) SearchValues – कुल 25.1 ms
  • Per-call SearchValues – कुल 70.2 ms

हर कॉल के लिए ऑब्जेक्ट बनाना पूरी प्रक्रिया को SearchValues का उपयोग न करने की तुलना में तीन गुना धीमा बना देता है। इंस्टेंस को static readonly फ़ील्ड में स्टोर करें या अन्य तरीकों से कॉल के दौरान इसे दोबारा इस्तेमाल करें।

SearchValues का उपयोग कब करें

  • बड़े इनपुट, कम मैच – वेक्टरकृत पथ (vectorized path) तब चमकता है जब स्कैनर बिना डेलीमिटर से टकराए लंबे हिस्सों को छोड़ सकता है।
  • एक ही सेट के साथ बार-बार स्कैन – यदि एक ही कैरेक्टर्स को कई बार खोजा जाता है, तो एक बार का सेटअप फायदेमंद होता है।

IndexOfAny के साथ कब बने रहें

  • छोटी स्ट्रिंग्स या अधिक मैच – अतिरिक्त सेटअप लागत मामूली गति लाभ से अधिक हो जाती है।
  • कोड जो पहले से ही IndexOfAny का उपयोग करता है – आधुनिक .NET का कार्यान्वयन पहले से ही छोटे कैरेक्टर सेट्स के लिए अत्यधिक ट्यून किया गया है, इसलिए SearchValues को अपनाने से कोई खास फर्क नहीं पड़ सकता है।

सामान्य गलतियाँ

  • हॉट लूप्स (hot loops) में कंस्ट्रक्शन को एम्बेड करना – इससे ऊपर दिखाए गए तीन गुना धीमेपन की समस्या होती है।
  • मैन्युअल लूप्स के साथ रनटाइम को "होशियार" बनाने की कोशिश करना – मैन्युअल foreach वर्शन दोनों बिल्ट-इन तरीकों की तुलना में दोगुना धीमा था, जो पुष्टि करता है कि SIMD निर्देशों के गहरे ज्ञान के बिना .NET लाइब्रेरीज़ को हराना कठिन है।
  • यूनिवर्सल स्पीडअप मान लेना – घने डेटा (dense data) पर आधा मिलीसेकंड की जीत यह साबित करती है कि SearchValues कोई यूनिवर्सल चीट कोड नहीं है; इसके लाभ डेटा पर निर्भर करते हैं।

निष्कर्ष

SearchValues<T> केवल तभी स्पष्ट लाभ देता है जब आप विरल (infrequent) कैरेक्टर्स के लिए बड़े बफ़र्स को स्कैन करते हैं और एक बार की कंस्ट्रक्शन लागत वहन कर सकते हैं। रोज़मर्रा के अधिकांश स्ट्रिंग-सर्च परिदृश्यों—छोटे इनपुट, बार-बार होने वाले मैच, या वह कोड जो पहले से ही IndexOfAny पर निर्भर है—के लिए, बिल्ट-इन मेथड सरल और तेज़ विकल्प बना हुआ है। SearchValues का चयनात्मक रूप से उपयोग करें, इंस्टेंस को कैश करें, और आप वास्तविक प्रदर्शन लाभ प्राप्त करते हुए छिपे हुए नुकसान से बचेंगे।

Source code and full test suite: https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l