تُظهر SearchValues<T> أخيراً تفوقاً ملموساً في .NET 10 عند فحص المخازن المؤقتة (buffers) الكبيرة بحثاً عن فواصل متباعدة (sparse delimiters)، حيث تكتمل عملية SearchValues في 3.3 مللي ثانية مقابل 5.6 مللي ثانية لعملية IndexOfAny على حمولة (payload) بحجم 32 ميجابايت.

سبب ظهور SearchValues

قدم .NET 8 ميزة SearchValues<T> لحساب مجموعة من الأحرف (أو البايتات) مسبقاً، مما يسمح لبيئة التشغيل (runtime) باختيار أسرع خوارزمية فحص لتلك المجموعة. قم ببناء الكائن مرة واحدة، ثم أعد استخدامه كلما احتجت لتحديد أي من القيم في span. قد تستخدم بيئة التشغيل تعليمات متجهة (vectorized instructions)، أو حلقات خالية من التفرع (branch-free loops)، أو حيل برمجية منخفضة المستوى أخرى لا يمكن للبدائل المكتوبة يدوياً مضاهاتها دون بذل جهد كبير.

اختبارات الأداء (Benchmarks) المهمة

استخدمت الاختبارات التي أثارت النقاش مخزناً مؤقتاً (buffer) بحجم 32 ميجابايت يحتوي على خمسة أحرف فواصل. تم قياس زمن ثلاثة أساليب في .NET 10:

  • IndexOfAny(char[]) – 10.2 مللي ثانية
  • حلقة foreach يدوية عبر الـ span – 19.4 مللي ثانية
  • SearchValues – 9.7 مللي ثانية

تفوقت SearchValues على IndexOfAny المدمجة بنصف مللي ثانية فقط. النتيجة ليست ادعاءً دراماتيكياً بأنها "أسرع بخمس مرات" كما يتم تداوله في المنتديات؛ فـ .NET الحديثة تقوم بالفعل بتحسين IndexOfAny للمجموعات الصغيرة، مما يقلص الفجوة.

عندما تكون الفواصل نادرة — تظهر مرة واحدة كل 40 كيلوبايت بدلاً من كل 60 بايت — تتغير الصورة:

  • IndexOfAny(char[]) – 5.6 مللي ثانية
  • SearchValues – 3.3 مللي ثانية

الآن أصبحت SearchValues أسرع بمقدار 1.7 × لأن المحرك يندفع عبر سلاسل طويلة من البيانات غير المتطابقة باستخدام خطوات متجهة (vectorized steps)، بينما تعود IndexOfAny إلى مسار أقل قوة.

التكلفة الخفية

ميزة SearchValues ليست مجانية. فبناء الكائن يستهلك ذاكرة ويبني جداول بحث داخلية. إذا قمت بإنشائه عند كل استدعاء للدالة، فإن العبء الإضافي (overhead) سيفوق أي مكسب في وقت التشغيل. أظهر اختبار أداء استدعى روتين الفحص مليون مرة على أسطر قصيرة ما يلي:

  • SearchValues ثابتة (معاد استخدامها) – 25.1 مللي ثانية إجمالاً
  • SearchValues لكل استدعاء – 70.2 مللي ثانية إجمالاً

إن إنشاء الكائن مع كل استدعاء يجعل العملية برمتها أبطأ بثلاث مرات مما لو لم يتم استخدام SearchValues على الإطلاق. قم بتخزين المثيل (instance) في حقل static readonly أو أعد استخدامه بطريقة أخرى عبر الاستدعاءات.

متى تلجأ إلى SearchValues

  • المدخلات الكبيرة، والمطابقات القليلة – يتألق المسار المتجه (vectorized path) عندما يتمكن الفاحص من تخطي مساحات طويلة دون الاصطدام بفاصل.
  • عمليات الفحص المتكررة لنفس المجموعة – إذا تم البحث عن نفس الأحرف مرات عديدة، فإن الإعداد الذي يتم لمرة واحدة يؤتي ثماره.

متى تكتفي بـ IndexOfAny

  • السلاسل القصيرة أو المطابقات الكثيرة – تكلفة الإعداد الإضافية تفوق مكسب السرعة الهامشي.
  • الكود الذي يستخدم IndexOfAny بالفعل – تنفيذ .NET الحديث مُحسّن للغاية بالفعل لمجموعات الأحرف الصغيرة، لذا فإن استبداله بـ SearchValues قد لا يحدث فرقاً ملموساً.

الأخطاء الشائعة

  • تضمين عملية البناء داخل حلقات التكرار السريعة (hot loops) – يؤدي إلى التباطؤ بمقدار ثلاثة أضعاف كما هو موضح أعلاه.
  • محاولة "التفوق ذكاءً" على بيئة التشغيل باستخدام حلقات يدوية – كانت نسخة foreach اليدوية أبطأ بمرتين من كلتا الطريقتين المدمجتين، مما يؤكد أنه من الصعب التغلب على مكتبات .NET دون معرفة عميقة بتعليمات SIMD.
  • افتراض وجود تسريع شامل – الفوز بنصف مللي ثانية في البيانات الكثيفة يثبت أن SearchValues ليست "شفرة غش" عالمية؛ ففوائدها تعتمد على طبيعة البيانات.

الخلاصة

توفر SearchValues<T> ميزة واضحة فقط عندما تقوم بفحص مخازن مؤقتة كبيرة بحثاً عن أحرف غير متكررة، وعندما تتحمل تكلفة البناء التي تتم لمرة واحدة. بالنسبة لغالبية سيناريوهات البحث عن النصوص اليومية — المدخلات القصيرة، أو المطابقات المتكررة، أو الكود الذي يعتمد بالفعل على IndexOfAny — تظل الطريقة المدمجة هي الخيار الأبسط والأسرع. استخدم SearchValues بشكل انتقائي، وقم بتخزين المثيل (cache the instance)، لتتجنب الضريبة الخفية مع جني مكاسب الأداء الحقيقية.

كود المصدر ومجموعة الاختبارات الكاملة: https://dev.to/ssukhpinder/when-searchvalues-actually-pays-off-310l