من برای شمارش هر کلمه، دو بار عملیات هش را انجام دادم

هفته گذشته یک تجزیه‌کننده لاگ (log parser) را پروفایل کردم. این برنامه کدهای خطا را در یک لاگ روزانه می‌شمارد. منطق اصلی آن یک به‌روزرسانی ساده در یک دیکشنری است.

اکثر توسعه‌دهندگان آن را به این صورت می‌نویسند:

if (counts.TryGetValue(code, out int c))
    counts[code] = c + 1;
else
    counts[code] = 1;

این کد، کلید را دو بار هش می‌کند. TryGetValue با محاسبه هش و پیمایش باکت (bucket)، ورودی را پیدا می‌کند. سپس ایندکسر (indexer) دقیقاً همان کار را دوباره برای به‌روزرسانی مقدار انجام می‌دهد. شما در هر توکن، چرخه‌های CPU را برای همان کلید و همان هش، دو بار هدر می‌دهید.

می‌توانید از مرحله دوم صرف‌نظر کنید. از CollectionsMarshal.GetValueRefOrAddDefault استفاده کنید.

ref int slot = ref CollectionsMarshal.GetValueRefOrAddDefault(counts, code, out _);
slot++;

این متد در یک مرحله، اسلات (slot) را پیدا کرده یا ایجاد می‌کند. این متد مستقیماً یک ارجاع (reference) به محل ذخیره‌سازی به شما می‌دهد. شما فقط یک بار عملیات هش و یک بار پیمایش باکت را انجام می‌دهید و سپس مقدار را در همان‌جا تغییر می‌دهید.

من این را با ۵ میلیون توکن تست کردم.

نتایج: • TryGetValue + indexer: 160.0 ms • GetValueRefOrAddDefault: 95.0 ms

نسخه با یک جستجو، ۱.۷ برابر سریع‌تر است.

نکته بسیار مهم این است که تخصیص حافظه (memory allocation) بدون تغییر باقی ماند. این ترفند حافظه را ذخیره نمی‌کند، بلکه فقط در مصرف CPU صرفه‌جویی می‌کند. اگر کد شما به دلیل Garbage Collection کند است، این تغییر تأثیری نخواهد داشت. اما اگر کد شما به دلیل عملیات سنگین شمارش کند است، این روش کمک می‌کند.

زمانی از این روش استفاده کنید که حلقه شما به‌روزرسانی‌های زیادی روی کلیدهای موجود انجام می‌دهد. هرچه نسبت به‌روزرسانی‌ها به درج‌های جدید (inserts) بیشتر باشد، میزان بهره‌وری بالاتر می‌رود.

یک هشدار: این ارجاع به محل ذخیره‌سازی داخلی دیکشنری اشاره دارد و تنها تا قبل از اولین تغییر ساختاری معتبر می‌ماند. اگر کلیدی را اضافه یا حذف می‌کنید، ارجاع را نگه ندارید. ارجاع را بگیرید، مقدار را تغییر دهید و از آن عبور کنید.

برای اکثر کارها از TryGetValue استفاده کنید، زیرا خواندن آن آسان‌تر است. تنها زمانی از نسخه ref استفاده کنید که حلقه دیکشنری شما به یک گلوگاه عملکردی (performance bottleneck) تبدیل شده باشد.

Source: https://dev.to/ssukhpinder/i-hashed-every-word-twice-to-count-it-once-1mg9