ביצעתי Hash לכל מילה פעמיים כדי לספור אותה פעם אחת

ביצעתי פרופיילינג למנתח לוגים בשבוע שעבר. הוא סופר קודי שגיאה בלוג יומי. הלוגיקה המרכזית היא עדכון פשוט של מילון (dictionary).

רוב המפתחים כותבים זאת כך:

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

הקוד הזה מבצע Hash למפתח פעמיים. TryGetValue מוצאת את הרשומה על ידי חישוב ה-hash ומעבר על ה-bucket. לאחר מכן, ה-indexer מבצע בדיוק את אותה עבודה שוב כדי לעדכן את הערך. אתם מבזבזים מחזורי CPU על אותו מפתח ואותו hash פעמיים עבור כל טוקן.

ניתן לדלג על הסיבוב השני. השתמשו ב-CollectionsMarshal.GetValueRefOrAddDefault.

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

השיטה הזו מוצאת או יוצרת את ה-slot בשלב אחד. היא נותנת לכם רפרנס (reference) ישירות לאחסון. אתם מבצעים hash אחד ומעבר bucket אחד. לאחר מכן, אתם משנים (mutate) את הערך במקום.

בדקתי זאת עם 5 מיליון טוקנים.

תוצאות: • TryGetValue + indexer: 160.0 ms • GetValueRefOrAddDefault: 95.0 ms

הגרסה עם החיפוש היחיד מהירה פי 1.7.

חשוב לציין, הקצאות הזיכרון נותרו זהות. הטריק הזה לא חוסך זיכרון. הוא חוסך רק CPU. אם הקוד שלכם איטי בגלל garbage collection, השינוי הזה לא יעשה דבר. אם הקוד שלכם איטי בגלל ספירה מאסיבית, זה עוזר.

השתמשו בזה כאשר הלולאה שלכם מבצעת הרבה עדכונים למפתחות קיימים. התועלת גדלה ככל שהיחס בין עדכונים להכנסות (inserts) עולה.

אזהרה: הרפרנס מצביע על האחסון הפנימי של המילון. הוא נשאר תקף רק עד לשינוי מבני (structural change) הבא. אל תחזיקו את הרפרנס אם אתם מוסיפים או מסירים מפתחות. קחו את ה-ref, שנו את הערך, והמשיכו הלאה.

השתמשו ב-TryGetValue עבור רוב המשימות. זה קל יותר לקריאה. השתמשו בגרסת ה-ref רק כאשר לולאת המילון שלכם היא צוואר בקבוק בביצועים (performance bottleneck).

מקור: https://dev.to/ssukhpinder/i-hashed-every-word-twice-to-count-it-once-1mg9