ഒരു തവണ എണ്ണാൻ വേണ്ടി ഓരോ വാക്കും ഞാൻ രണ്ടുതവണ ഹാഷ് ചെയ്തു

കഴിഞ്ഞ ആഴ്ച ഞാൻ ഒരു ലോഗ് പാഴ്സറിനെ (log parser) പ്രൊഫൈൽ ചെയ്തു. ഇത് ഒരു ദിവസത്തെ ലോഗിലെ എറർ കോഡുകൾ (error codes) എണ്ണുന്നു. ഇതിന്റെ പ്രധാന ലോജിക് ഒരു ലളിതമായ ഡിക്ഷനറി അപ്‌ഡേറ്റ് (dictionary update) ആണ്.

മിക്ക ഡെവലപ്പർമാരും ഇത് ഇപ്രകാരമാണ് എഴുതുന്നത്:

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

ഈ കോഡ് കീ (key) രണ്ടുതവണ ഹാഷ് ചെയ്യുന്നു. TryGetValue ഹാഷ് കണക്കാക്കിയും ബക്കറ്റ് (bucket) പരിശോധിച്ചും എൻട്രി കണ്ടെത്തുന്നു. തുടർന്ന്, വാല്യൂ അപ്‌ഡേറ്റ് ചെയ്യാൻ ഇൻഡെക്സർ (indexer) അതേ ജോലി വീണ്ടും ചെയ്യുന്നു. ഓരോ ടോക്കണിനും (token) ഒരേ കീയിലും ഒരേ ഹാഷിലും നിങ്ങൾ രണ്ടുതവണ CPU സൈക്കിളുകൾ പാഴാക്കുന്നു.

നിങ്ങൾക്ക് രണ്ടാമത്തെ ഘട്ടം ഒഴിവാക്കാം. CollectionsMarshal.GetValueRefOrAddDefault ഉപയോഗിക്കുക.

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

ഈ മെത്തേഡ് ഒറ്റ ഘട്ടത്തിൽ തന്നെ സ്ലോട്ട് (slot) കണ്ടെത്തുകയോ അല്ലെങ്കിൽ പുതിയത് നിർമ്മിക്കുകയോ ചെയ്യുന്നു. ഇത് സ്റ്റോറേജിലേക്കുള്ള (storage) നേരിട്ടുള്ള ഒരു റഫറൻസ് (reference) നിങ്ങൾക്ക് നൽകുന്നു. നിങ്ങൾ ഒരു ഹാഷും ഒരു ബക്കറ്റ് വാക്കും (bucket walk) മാത്രമേ ചെയ്യുന്നുള്ളൂ. തുടർന്ന് വാല്യൂ നേരിട്ട് മാറ്റം വരുത്താം (mutate).

ഞാൻ ഇത് 5 മില്യൺ ടോക്കണുകൾ ഉപയോഗിച്ച് പരീക്ഷിച്ചു.

ഫലങ്ങൾ: • TryGetValue + indexer: 160.0 ms • GetValueRefOrAddDefault: 95.0 ms

ഒറ്റ ലുക്കപ്പ് (one-lookup) വേർഷൻ 1.7 മടങ്ങ് വേഗതയുള്ളതാണ്.

പ്രധാനമായും, മെമ്മറി അലോക്കേഷനുകളിൽ (memory allocations) മാറ്റമൊന്നും വന്നില്ല. ഈ വിദ്യ മെമ്മറി ലാഭിക്കില്ല, പകരം CPU മാത്രമേ ലാഭിക്കൂ. ഗാർബേജ് കളക്ഷൻ (garbage collection) കാരണമാണ് നിങ്ങളുടെ കോഡ് പതുക്കെ പ്രവർത്തിക്കുന്നതെങ്കിൽ, ഈ മാറ്റം കൊണ്ട് ഒരു ഗുണവുമില്ല. എന്നാൽ കനത്ത കൗണ്ടിംഗ് (heavy counting) കാരണമാണ് കോഡ് പതുക്കെയാണെങ്കിൽ, ഇത് സഹായിക്കും.

നിലവിലുള്ള കീകൾക്ക് (existing keys) ഒരു ലൂപ്പ് വഴി ധാരാളം അപ്‌ഡേറ്റുകൾ നടത്തുമ്പോഴാണ് ഇത് ഉപയോഗിക്കേണ്ടത്. ഇൻസെർട്ടുകളേക്കാൾ (inserts) അപ്‌ഡേറ്റുകളുടെ അനുപാതം കൂടുന്തോറും ഇതിന്റെ ഗുണം വർദ്ധിക്കുന്നു.

ഒരു മുന്നറിയിപ്പ്: ഈ റഫറൻസ് ഡിക്ഷനറിയുടെ ആന്തരിക സ്റ്റോറേജിലേക്കാണ് (internal dictionary storage) ചൂണ്ടിക്കാണിക്കുന്നത്. അടുത്ത ഘടനാപരമായ മാറ്റം (structural change) ഉണ്ടാകുന്നത് വരെ മാത്രമേ ഇത് സാധുതയുള്ളതാകൂ. നിങ്ങൾ കീകൾ കൂട്ടുകയോ നീക്കം ചെയ്യുകയോ ചെയ്യുന്നുണ്ടെങ്കിൽ ഈ റഫറൻസ് കൈവശം വെക്കരുത്. റഫറൻസ് എടുക്കുക, വാല്യൂ മാറ്റുക, എന്നിട്ട് മുന്നോട്ട് പോവുക.

മിക്ക ജോലികൾക്കും TryGetValue ഉപയോഗിക്കുക. അത് വായിക്കാൻ എളുപ്പമാണ്. നിങ്ങളുടെ ഡിക്ഷനറി ലൂപ്പ് ഒരു പെർഫോമൻസ് ബോട്ടിൽനെക്ക് (performance bottleneck) ആയി മാറുമ്പോൾ മാത്രം ref വേർഷൻ ഉപയോഗിക്കുക.

ഉറവിടം: https://dev.to/ssukhpinder/i-hashed-every-word-twice-to-count-it-once-1mg9