لقد قمت بعمل Hash لكل كلمة مرتين لأقوم بعدّها مرة واحدة
قمت بتحليل أداء (profiling) محلل سجلات (log parser) الأسبوع الماضي. يقوم هذا المحلل بعدّ رموز الخطأ في سجل يومي. المنطق الأساسي هو تحديث بسيط لقاموس (dictionary).
معظم المطورين يكتبونها بهذا الشكل:
if (counts.TryGetValue(code, out int c))
counts[code] = c + 1;
else
counts[code] = 1;
يقوم هذا الكود بعمل hash للمفتاح مرتين. تقوم TryGetValue بالعثور على المدخل عن طريق حساب الـ hash والتنقل عبر الـ bucket. ثم يقوم الـ indexer بنفس العمل تماماً مرة أخرى لتحديث القيمة. أنت تهدر دورات المعالج (CPU cycles) على نفس المفتاح ونفس الـ hash مرتين لكل token.
يمكنك تخطي الرحلة الثانية. استخدم CollectionsMarshal.GetValueRefOrAddDefault.
ref int slot = ref CollectionsMarshal.GetValueRefOrAddDefault(counts, code, out _);
slot++;
تجد هذه الطريقة أو تنشئ الـ slot في خطوة واحدة. فهي تمنحك مرجعاً (reference) مباشرة إلى مكان التخزين. تقوم بعمل hash واحد وتنقل واحد عبر الـ bucket، ثم تقوم بتعديل القيمة في مكانها.
اختبرت هذا مع 5 ملايين token.
النتائج: • TryGetValue + indexer: 160.0 ms • GetValueRefOrAddDefault: 95.0 ms
النسخة التي تعتمد على عملية بحث واحدة أسرع بمقدار 1.7 مرة.
والأهم من ذلك، ظلت تخصيصات الذاكرة (memory allocations) متطابقة. هذه الحيلة لا توفر الذاكرة، بل توفر المعالج فقط. إذا كان الكود الخاص بك بطيئاً بسبب الـ garbage collection، فلن يفعل هذا التغيير شيئاً. أما إذا كان الكود بطيئاً بسبب عمليات العد الكثيفة، فهذا سيساعد.
استخدم هذا عندما تقوم الحلقة (loop) بإجراء تحديثات كثيرة على مفاتيح موجودة بالفعل. تزداد الفائدة كلما زادت نسبة التحديثات إلى عمليات الإدراج.
تحذير: يشير المرجع إلى مكان التخزين الداخلي للقاموس. ويظل صالحاً فقط حتى التغيير الهيكلي التالي. لا تحتفظ بالمرجع إذا كنت ستضيف أو تحذف مفاتيح. احصل على الـ ref، غيّر القيمة، ثم تابع عملك.
استخدم TryGetValue لمعظم المهام، فهي أسهل في القراءة. استخدم نسخة الـ ref فقط عندما تكون حلقة القاموس تمثل عنق زجاجة (bottleneck) في الأداء.
Source: https://dev.to/ssukhpinder/i-hashed-every-word-twice-to-count-it-once-1mg9
