SearchValues<T> سرانجام در .NET 10 هنگام اسکن بافرهای بزرگ برای یافتن جداکنندههای پراکنده (sparse delimiters)، برتری قابلاندازهگیری نشان میدهد؛ بهطوری که SearchValues در ۳.۳ میلیثانیه کار خود را تمام میکند، در حالی که اجرای IndexOfAny روی یک محموله (payload) ۳۲ مگابایتی، ۵.۶ میلیثانیه زمان میبرد.
چرا SearchValues معرفی شد
.NET 8 قابلیت SearchValues<T> را برای پیشمحاسبه مجموعهای از کاراکترها (یا بایتها) معرفی کرد تا زمان اجرا (runtime) بتواند سریعترین الگوریتم اسکن را برای آن مجموعه انتخاب کند. شیء را یک بار بسازید و سپس هر زمان که نیاز داشتید مقادیر را در یک span پیدا کنید، از آن دوباره استفاده کنید. زمان اجرا ممکن است از دستورالعملهای وکتوریزه شده (vectorized instructions)، حلقههای بدون شاخه (branch-free loops) یا سایر ترفندهای سطح پایین استفاده کند که جایگزینهای دستی بدون تلاش بسیار زیاد نمیتوانند با آنها رقابت کنند.
بنچمارکهای مهم
آزمایشهایی که باعث ایجاد این بحث شدند، از یک بافر ۳۲ مگابایتی حاوی پنج کاراکتر جداکننده استفاده کردند. زمان سه روش در .NET 10 اندازهگیری شد:
- IndexOfAny(char[]) – 10.2 ms
- حلقه foreach دستی روی
span– 19.4 ms - SearchValues
– 9.7 ms
SearchValues تنها با اختلاف نیم میلیثانیه از IndexOfAny داخلی پیشی گرفت. این نتیجه، آن ادعای دراماتیک «پنج برابر سریعتر» که در انجمنها شنیده میشود را تأیید نمیکند؛ .NET مدرن از قبل IndexOfAny را برای مجموعههای کوچک بهینه کرده است و این شکاف را کم کرده است.
وقتی جداکنندهها نادر باشند — یعنی مثلاً هر ۴۰ کیلوبایت یک بار ظاهر شوند به جای هر ۶۰ بایت — تصویر تغییر میکند:
- IndexOfAny(char[]) – 5.6 ms
- SearchValues
– 3.3 ms
اکنون SearchValues حدود ۱.۷ برابر سریعتر است، زیرا موتور با استفاده از گامهای وکتوریزه شده، از میان بخشهای طولانی دادههای غیرمطابقتیافته با سرعت عبور میکند، در حالی که IndexOfAny به مسیری با شدت کمتر بازمیگردد.
هزینه پنهان
استفاده از SearchValues رایگان نیست. ساختن این شیء باعث تخصیص حافظه و ایجاد جداول جستجوی داخلی میشود. اگر آن را در هر فراخوانی متد نمونهسازی (instantiate) کنید، سربار آن بر هرگونه برد زمانی غلبه خواهد کرد. بنچمارکی که روتین اسکن را یک میلیون بار روی خطوط کوتاه فراخوانی کرد، نشان داد:
- SearchValues استاتیک (بازاستفاده شده) – مجموعاً 25.1 ms
- SearchValues در هر فراخوانی – مجموعاً 70.2 ms
ساختن شیء در هر فراخوانی، کل عملیات را سه برابر کندتر از زمانی میکند که اصلاً از SearchValues استفاده نکنید. نمونه را در یک فیلد static readonly ذخیره کنید یا به روش دیگری در طول فراخوانیها از آن مجدداً استفاده کنید.
چه زمانی از SearchValues استفاده کنیم
- ورودیهای بزرگ، تطابقهای کم – مسیر وکتوریزه شده زمانی میدرخشد که اسکنر بتواند از بخشهای طولانی بدون برخورد با یک جداکننده عبور کند.
- اسکنهای مکرر با یک مجموعه یکسان – اگر کاراکترهای یکسان بارها جستجو شوند، هزینه تنظیمات اولیه (که فقط یکبار انجام میشود) جبران خواهد شد.
چه زمانی از IndexOfAny استفاده کنیم
- رشتههای کوتاه یا تطابقهای زیاد – هزینه اضافی تنظیمات اولیه بر سود ناچیز سرعت غلبه میکند.
- کدهایی که در حال حاضر از IndexOfAny استفاده میکنند – پیادهسازی
.NETمدرن از قبل برای مجموعههای کاراکتری کوچک بسیار بهینه شده است، بنابراین جایگزین کردن آن باSearchValuesممکن است تغییر چندانی ایجاد نکند.
اشتباهات رایج
- قرار دادن فرآیند ساخت در حلقههای پرکاربرد (hot loops) – منجر به کندی سه برابری میشود که در بالا نشان داده شد.
- تلاش برای «هوشمندتر بودن» از زمان اجرا با حلقههای دستی – نسخه
foreachدستی دو برابر کندتر از هر دو روش داخلی بود، که تأیید میکند شکست دادن کتابخانههای.NETبدون دانش عمیق از دستورالعملهایSIMDکار دشواری است. - فرض کردن افزایش سرعت همگانی – برد نیم میلیثانیهای در دادههای متراکم ثابت میکند که
SearchValuesیک کد تقلب همگانی نیست؛ مزایای آن به نوع داده بستگی دارد.
نتیجهگیری
فقط زمانی SearchValues<T> مزیت آشکاری ارائه میدهد که بافرهای بزرگ را برای یافتن کاراکترهای غیرمتناوب اسکن کنید و بتوانید هزینه ساخت یکباره آن را بپردازید. برای اکثر سناریوهای روزمره جستجوی رشته — ورودیهای کوتاه، تطابقهای مکرر، یا کدهایی که از قبل به IndexOfAny متکی هستند — روش داخلی همچنان انتخاب سادهتر و سریعتر است. از SearchValues بهصورت گزینشی استفاده کنید، نمونه را کش (cache) کنید و با این کار از جریمه پنهان جلوگیری کرده و در عین حال از برد واقعی عملکرد بهرهمند خواهید شد.
کد منبع و مجموعه کامل تستها: https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l
