در حین توسعه APIهای پرترافیک، بهینهسازی کوئریهای دیتابیس و لایههای کش، معمولاً در صدر اولویتها قرار دارند. اما یک دشمن پنهان وجود دارد که به آرامی بودجه CPU شما را میبلعد: عملیات رشتهای. هزاران جستجو در رشتهها در هر ثانیه، بدون اینکه در داشبورد APM شما فریاد بزنند، جمع میشوند و با افزایش بار، به صورت خطی هزینهها را بالا میبرند.
یک نمونه: توسعهی یک API پردازش لاگ. گلوگاه؟ اعتبارسنجی و پاکسازی رشتهها. راهحل؟ یک قابلیت جدید در .NET 8 به نام SearchValues که نه تنها عملکرد را ۵ برابر بهبود میبخشد، بلکه اجازه میدهد بهجای افزودن سرور، چند ماشین را خاموش کنیم. این بهبود در .NET 9 و .NET 10 حتی چشمگیرتر شده است.
مسئله واقعی: عملیات رشتهای به شکلی که فکر میکنید مقیاس نمیشوند
دو برابر شدن ترافیک = دو برابر شدن مصرف CPU. افزودن قابلیتهای جدید که رشتهها را تحلیل میکنند = چند برابر شدن مشکل.
روشهای سنتی مثل string.IndexOfAny() یا حلقههای سفارشی، در بارهای کم (دهها درخواست در ثانیه) خوب کار میکنند. اما در هزاران درخواست در ثانیه، به آرامی مشکلساز میشوند. برای نمونه در یک سرویس تجمیع لاگ، که لاگهای برنامه را در زمان واقعی پردازش میکند، در هر لاگ نیاز به:
- پاکسازی (Sanitization)
- تشخیص دادههای حساس
- اعتبارسنجی
است. این یعنی چندین عملیات رشتهای برای هر لاگ. هزاران لاگ در ثانیه یعنی میلیونها عملیات در دقیقه.
روشهای قدیمی از SIMD (دستورات موازی پردازش داده) به صورت مداوم استفاده نمیکنند. اما CPUهای مدرن میتوانند چندین کاراکتر را همزمان بررسی کنند. IndexOfAny() یک راهحل عمومی است، در حالی که این مسئله از تخصصیسازی سود میبرد.
راهحل قدیمی: IndexOfAny و حلقههای دستی
char[] delimiters = { ',', ';', '|' };
int index = logMessage.IndexOfAny(delimiters);این کد ساده است، اما:
- در هر فراخوانی، آرایه را میسازد (Allocation)
- از SIMD به صورت بهینه استفاده نمیکند
- با افزایش تعداد کاراکترهای جستجو، عملکرد خطی افت میکند
در بار بالا، این الگو به یک گلوگاه پنهان تبدیل میشود.
راهحل جدید:SearchValues<T>؛ بهینهسازی هوشمند و یکبار مصرف
SearchValues در .NET 8 معرفی شد و هنوز بسیاری از توسعهدهندگان از آن بیخبرند. چطور کار میکند؟
هنگام ساخت نمونه، .NET مجموعه مقادیر شما را یک بار تحلیل میکند و بهترین الگوریتم را انتخاب میکند:
| مجموعه | الگوریتم بهینه |
| کاراکترهای متراکم | Bitmap Lookup |
| مجموعههای پراکنده | شاخهبندی بهینه |
| مجموعههای بزرگ | SIMD Vectorization |
- هزینه تحلیل: یک بار در زمان راهاندازی
- هزینه جستجو: بسیار ارزان در میلیونها عملیات
private static readonly SearchValues<char> Delimiters =
SearchValues.Create([',', ';', '|']);سپس:
ReadOnlySpan<char> span = logMessage.AsSpan();
int index = span.IndexOfAny(Delimiters);
نکته کلیدی: نمونه را static readonly نگه دارید. ساخت مکرر در حلقههای داغ، مزیت را نابود میکند.
.NET 9: جستجوی چند زیررشتهای
در .NET 8 فقط SearchValues از نوعهای char و byte را داشتیم. نوع string آن در .NET 9 اضافه شد:
// قبل (Regex)
var regex = new Regex("(?i)error|warning|critical", RegexOptions.Compiled);
bool found = regex.IsMatch(logMessage);
// بعد (SearchValues)
private static readonly SearchValues<string> LogKeywords =
SearchValues.Create(["error", "warning", "critical"], StringComparison.OrdinalIgnoreCase);
bool found = logMessage.AsSpan().ContainsAny(LogKeywords);نتیجه: ۳ تا ۴ برابر سریعتر در تحلیل لاگ.
حتی کامپایلر Regex در .NET 9 به صورت خودکار از SearchValues استفاده میکند.
.NET 10: بهینهسازی در سطح سختافزار
- AVX-512: جایگزینی دو دستور با
PermuteVar64x8x2 با خروجی نصف شدن چرخههای CPU - ARM64: دستورات
UnzipEven ارزانتر برای عملکرد بهتر در Graviton و Ampere - جستجوی Case-Insensitive: منطق Fast-Path گستردهتر
مزیت ارتقاء از .NET 9 به .NET 10 → خروجی ۱۰-۲۰٪ سریعتر بدون تغییر کد
نمونه واقعی: پاکسازی لاگ بدون مالیات عملکرد
using System;
using System.Buffers;
using System.Text;
public class LogSanitizer
{
private static readonly SearchValues<char> DangerousCharacters =
SearchValues.Create("\0\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0E\x0F" +
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F");
private static readonly SearchValues<char> SensitiveMarkers =
SearchValues.Create("=:@");
public static string SanitizeLogEntry(string logEntry)
{
ReadOnlySpan<char> span = logEntry.AsSpan();
// مسیر سریع: اگر کاراکتر خطرناک نباشد
if (!span.ContainsAny(DangerousCharacters))
{
return ProcessSensitiveData(logEntry);
}
// مسیر کند: بازسازی بدون کاراکترهای خطرناک
var builder = new StringBuilder(logEntry.Length);
foreach (char c in span)
{
if (!DangerousCharacters.Contains(c))
builder.Append(c);
}
return ProcessSensitiveData(builder.ToString());
}
private static string ProcessSensitiveData(string log)
{
ReadOnlySpan<char> span = log.AsSpan();
int markerIndex = span.IndexOfAny(SensitiveMarkers);
if (markerIndex == -1) return log;
var result = new StringBuilder(log.Length);
result.Append(span.Slice(0, markerIndex + 1));
result.Append("[REDACTED]");
// در نسخه واقعی، 20 کاراکتر بعدی را حذف کنید
return result.ToString();
}
}نتایج تولید:
| معیار | قبل | بعد | بهبود |
| زمان پردازش ۱۰٬۰۰۰ لاگ | ~۴۵۰ میلیثانیه | ~۸۵ میلیثانیه | ۵.۳ برابر |
| زمان پاکسازی | - | - | ۸۷٪ کاهش |
| تخصیص حافظه (مسیر تمیز) | دارد | صفر | GC خوابیده |
چه زمانی ازSearchValues<T>استفاده کنیم؟
| شرایط | توصیه |
| جستجو برای ۳+ کاراکتر/مقدار | استفاده کنید |
| کد در مسیر داغ (per-request, حلقه تنگ) | ایدهآل |
| ورودی متغیر طول | سود بیشتر |
| CPU با SIMD (۵-۷ سال اخیر) | ۵-۱۰ برابر سریعتر |
| جستجوی ۱-۲ کاراکتر | IndexOf کافی است |
| اجرای یکبار (startup, error path) | ارزش ندارد |
| گلوگاه I/O | اولویت نیست |
نکته: هشدار CA1870: این تحلیلگر کد، امکان تبدیل IndexOfAny(array) را به SearchValues هشدار میدهد. اما همیشه پروفایل کنید؛ تحلیلگر جایگزین پروفایلر نیست.
نتیجهگیری: بهینهسازی نامرئی، تأثیر واقعی
SearchValues یکی از بیشترین ROIهای بهینهسازی نسبت به تلاش است:
- تغییر کد کم
- بدون تغییر معماری
- بدون بهروزرسانی وابستگی
- سود فوری و قابل اندازهگیری
اما فقط وقتی که الگو درست باشد:
جستجوی مکرر + چندین مقدار + ورودی متغیر طول
این الگو در همه جا هست:
- پردازش لاگ
- اعتبارسنجی ورودی
- تجزیه CSV
- فیلتر امنیتی
- پاکسازی مسیر فایل
وقتی این الگو را در کدتان پیدا کردید:
- اندازهگیری کنید (نه حدس)
SearchValues را اعمال کنید- با داده واقعی تست بار کنید
- تأثیر را در هزینه زیرساخت ببینید
بهینهسازی خوب، برای کاربر نامرئی است، اما در متریکهای عملیاتی کاملاً مشهود. SearchValues یک ابزار نیست؛ یک تغییر دهنده بازی در مقیاس است.