I Hashed Every Word Twice to Count It Once

I profiled a log parser last week. It counts error codes in a daily log. The core logic is a simple dictionary update.

Most developers write it like this:

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

This code hashes the key twice. TryGetValue finds the entry by computing the hash and walking the bucket. Then the indexer does the exact same work again to update the value. You waste CPU cycles on the same key and same hash twice per token.

You can skip the second trip. Use CollectionsMarshal.GetValueRefOrAddDefault.

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

This method finds or creates the slot in one step. It gives you a reference directly to the storage. You perform one hash and one bucket walk. Then you mutate the value in place.

I tested this with 5 million tokens.

Results: • TryGetValue + indexer: 160.0 ms • GetValueRefOrAddDefault: 95.0 ms

The one-lookup version is 1.7x faster.

Crucially, memory allocations remained identical. This trick does not save memory. It only saves CPU. If your code is slow because of garbage collection, this change does nothing. If your code is slow because of heavy counting, this helps.

Use this when your loop performs many updates to existing keys. The benefit grows as the ratio of updates to inserts increases.

A warning: The reference points to internal dictionary storage. It only stays valid until the next structural change. Do not hold the reference if you add or remove keys. Grab the ref, change the value, and move on.

Use TryGetValue for most tasks. It is easier to read. Use the ref version only when your dictionary loop is a performance bottleneck.

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