7.8 ميجابايت من المفاتيح خصصتها فقط لأهدرها

كشف أداة تحليل الأداء (profiler) عن تكلفة خفية في الكود الخاص بي.

يقوم معالج الطلبات (request handler) الخاص بي بقراءة الرموز (tokens) من سلسلة نصية ضخمة ويجمع أوزانها. المنطق البرمجي كان يعمل، لكن الذاكرة لم تكن كذلك.

داخل حلقة تكرارية نشطة (hot loop)، قمت بتخصيص 7.8 ميجابايت من السلاسل النصية — مفتاح جديد لكل عملية بحث في القاموس (dictionary lookup) — ثم تخلصت من المفتاح فوراً.

كان المسبب هو Substring. فكل جزء (slice) كان ينشئ كائن سلسلة نصية جديداً.

200,000 رمز (token)

  • الطريقة A (Substring): 23.4 مللي ثانية، تم تخصيص 7,812 كيلوبايت.
  • الطريقة B (Span Lookup): 14.9 مللي ثانية، تم تخصيص 0 كيلوبايت.

الطريقة B أسرع بمقدار 1.5 مرة ولا تُنتج أي نفايات (garbage).

في .NET 9، يمكنك استدعاء GetAlternateLookup. فبدلاً من إنشاء سلسلة نصية جديدة، تمرر ReadOnlySpan<char>، والتي ليست سوى نافذة على السلسلة الأصلية — دون الحاجة للنسخ.

يقوم القاموس بعمل hash للـ span مباشرة مقابل المفاتيح الموجودة، مما يعطي نفس النتيجة دون الحاجة لتخصيص ذاكرة.

أشياء يجب تذكرها

  • تعمل مع StringComparer.Ordinal و StringComparer.OrdinalIgnoreCase.
  • تسبب خطأً (throws) أثناء وقت التشغيل إذا استخدمت مقارناً مخصصاً (custom comparer) يفتقر إلى دعم البحث البديل (alternate lookup).
  • مثالية للمسارات النشطة (hot paths) مثل المحللات (parsers)، أو معالجات السجلات (log processors)، أو ماسحات ملفات CSV.
  • تجنب استخدامها في عمليات البحث البسيطة التي تحتوي على مفاتيح قليلة فقط.

أصبحت الآن أطارد في الكود الخاص بي نمط Substring متبوعاً بـ TryGetValue. هذا النمط يهدر كميات هائلة من الذاكرة.

المصدر: https://dev.to/ssukhpinder/78-mb-of-keys-i-allocated-just-to-throw-away-50mn

مجتمع تعليمي اختياري: https://github.com/ssukhpinder/dev-to-code-samples/tree/main/023-dictionary-alternate-lookup