বড় বাফার থেকে বিরল ডেলিমিটার (sparse delimiters) স্ক্যান করার ক্ষেত্রে .NET 10-এ SearchValues<T> অবশেষে একটি পরিমাপযোগ্য সুবিধা প্রদর্শন করছে; একটি 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
- Manual foreach loop over the span – 19.4 ms
- SearchValues
– 9.7 ms
SearchValues বিল্ট-ইন IndexOfAny-কে মাত্র আধা মিলিসেকেন্ড ব্যবধানে হারিয়েছে। ফলাফলটি ফোরামগুলোতে প্রচলিত সেই নাটকীয় "পাঁচ গুণ দ্রুততর" দাবির মতো নয়; আধুনিক .NET ইতিমধ্যেই ছোট সেটের জন্য IndexOfAny-কে অপ্টিমাইজ করে রেখেছে, যা এই ব্যবধান কমিয়ে দেয়।
যখন ডেলিমিটারগুলো বিরল হয়—প্রতি ৬০ বাইটের পরিবর্তে প্রতি ৪০ KB-তে একবার দেখা দেয়—তখন চিত্রটি বদলে যায়:
- IndexOfAny(char[]) – 5.6 ms
- SearchValues
– 3.3 ms
এখন SearchValues ১.৭ গুণ দ্রুততর, কারণ ইঞ্জিনটি ভেক্টরাইজড ধাপের মাধ্যমে মিলহীন ডেটার দীর্ঘ অংশগুলো দ্রুত অতিক্রম করতে পারে, যেখানে IndexOfAny কম আক্রমণাত্মক (less aggressive) পথে ফিরে যায়।
লুকানো খরচ
SearchValues ব্যবহারের কোনো খরচ নেই এমন নয়। অবজেক্টটি তৈরি করতে মেমরি বরাদ্দ করতে হয় এবং ইন্টারনাল লুকআপ টেবিল তৈরি করতে হয়। আপনি যদি প্রতিটি মেথড কলের সময় এটি ইনস্ট্যানশিয়েট (instantiate) করেন, তবে এর ওভারহেড রানটাইমের যেকোনো সুবিধাকে ছাপিয়ে যাবে। ছোট লাইনে স্ক্যানিং রুটিনটি দশ লক্ষ বার কল করে করা একটি বেঞ্চমার্কে দেখা গেছে:
- Static (reused) SearchValues – 25.1 ms total
- Per-call SearchValues – 70.2 ms total
প্রতিবার কল করার সময় অবজেক্টটি তৈরি করলে পুরো অপারেশনটি SearchValues ব্যবহার না করার চেয়েও তিনগুণ ধীর হয়ে যায়। তাই ইনস্ট্যান্সটি একটি static readonly ফিল্ডে সংরক্ষণ করুন অথবা কলগুলোর মধ্যে পুনরায় ব্যবহার করুন।
কখন SearchValues ব্যবহার করবেন
- বড় ইনপুট, কম ম্যাচ – ভেক্টরাইজড পাথটি তখন দারুণ কাজ করে যখন স্ক্যানারটি ডেলিমিটার না পেয়ে দীর্ঘ অংশগুলো এড়িয়ে যেতে পারে।
- একই সেটের বারবার স্ক্যান – যদি একই ক্যারেক্টার অনেকবার খোঁজা হয়, তবে একবারের সেটআপ অনেক উপকারে আসে।
কখন IndexOfAny ব্যবহার করা উচিত
- ছোট স্ট্রিং বা অনেক ম্যাচ – অতিরিক্ত সেটআপ খরচ সামান্য গতি বৃদ্ধির চেয়ে বেশি হয়ে দাঁড়ায়।
- যে কোডে ইতিমধ্যে IndexOfAny ব্যবহৃত হচ্ছে – আধুনিক .NET-এর ইমপ্লিমেন্টেশন ছোট ক্যারেক্টার সেটের জন্য ইতিমধ্যেই অত্যন্ত টিউন করা, তাই
SearchValues-এ পরিবর্তন আনলে খুব একটা পার্থক্য নাও হতে পারে।
সাধারণ ভুলসমূহ
- হট লুপের (hot loops) ভেতরে অবজেক্ট তৈরি করা – এর ফলে উপরে দেখানো তিনগুণ ধীরগতির সমস্যা দেখা দেয়।
- ম্যানুয়াল লুপ দিয়ে রানটাইমকে "ছকে ফেলার" চেষ্টা করা – ম্যানুয়াল
foreachভার্সনটি উভয় বিল্ট-ইন মেথডের চেয়ে দ্বিগুণ ধীরগতির ছিল, যা নিশ্চিত করে যে SIMD ইনস্ট্রাকশন সম্পর্কে গভীর জ্ঞান ছাড়া .NET লাইব্রেরিগুলোকে হারানো কঠিন। - সর্বজনীন গতি বৃদ্ধির ধারণা করা – ঘন ডেটার (dense data) ক্ষেত্রে মাত্র আধা মিলিসেকেন্ডের জয় প্রমাণ করে যে
SearchValuesকোনো সর্বজনীন চিট কোড নয়; এর সুবিধা ডেটার ওপর নির্ভর করে।
সারসংক্ষেপ
SearchValues<T> তখনই স্পষ্ট সুবিধা দেয় যখন আপনি বড় বাফার থেকে বিরল ক্যারেক্টার স্ক্যান করেন এবং একবারের অবজেক্ট তৈরির খরচ বহন করতে পারেন। দৈনন্দিন স্ট্রিং-সার্চের বেশিরভাগ ক্ষেত্রে—যেমন ছোট ইনপুট, ঘন ঘন ম্যাচ, বা যে কোডে ইতিমধ্যে IndexOfAny ব্যবহৃত হচ্ছে—বিল্ট-ইন মেথডটিই সহজ এবং দ্রুততর পছন্দ হিসেবে থাকে। SearchValues বেছে বেছে ব্যবহার করুন, ইনস্ট্যান্সটি ক্যাশ (cache) করে রাখুন, তাহলে আপনি লুকানো পেনাল্টি এড়িয়ে প্রকৃত পারফরম্যান্সের সুবিধা নিতে পারবেন।
সোর্স কোড এবং সম্পূর্ণ টেস্ট স্যুট: https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l
