Tips for writing a good hash function.

A Hash Function Has One Job and It Is Not Speed

I spent three years in high-frequency trading watching production systems choke on “optimized” lookup tables that were supposed to be lightning-fast. Most tutorials treat writing a good hash function like a math homework assignment, throwing complex polynomial rolling hashes at you as if complexity equals quality. They ignore the reality of the hardware. If your hash function produces a beautiful distribution but causes a cache miss every time it’s called, you haven’t written a high-performance tool; you’ve written a bottleneck disguised as an optimization.

I’m not here to teach you the abstract algebra of collision resistance. I want to talk about how your code actually interacts with the CPU and the memory hierarchy. I will show you how to balance mathematical distribution against the brutal reality of instruction latency and cache locality. We are going to skip the academic fluff and focus on the implementation details that actually matter when you’re trying to squeeze every last cycle out of a tight loop.

Table of Contents

Uniform Distribution Properties or Total Performance Collapse

Uniform Distribution Properties or Total Performance Collapse.

Most developers treat a hash function as a black box: you throw data in, you get a `size_t` out, and you move on. This is a mistake. If your function lacks strong uniform distribution properties, you aren’t just losing a bit of efficiency; you are actively sabotaging your data structures. When keys cluster in a narrow range of buckets, your $O(1)$ lookup time effectively decays into $O(n)$ linear scans. I’ve seen production systems grind to a halt because a developer used a naive modulo-based approach that failed to account for patterns in the input data.

The goal is to achieve a high-quality avalanche effect, where a single bit change in the input flips roughly half the bits in the output. Without this, your hash table becomes a series of long, expensive collision chains. You don’t necessarily need a heavy cryptographic vs non-cryptographic hash distinction here—you aren’t trying to prevent an adversary from reversing your keys—but you do need a deterministic mapping algorithm that spreads entropy effectively. If your bits don’t dance, your performance won’t either.

The Avalanche Effect Why Small Changes Must Matter

The Avalanche Effect Why Small Changes Must Matter

If you change a single bit in your input—say, flipping the least significant bit of an integer—and your output only changes by one bit in response, you haven’t written a hash function; you’ve written a glorified counter. This is where the avalanche effect in hashing becomes a non-negotiable requirement. In a robust implementation, that single-bit flip should trigger a cascade of changes that propagates through the entire output bitmask. Without this, you aren’t actually spreading your data; you’re just creating clusters.

When your hash lacks this sensitivity, you end up with massive clusters in your buckets, turning your $O(1)$ lookup into a linear search through a linked list. This isn’t just a theoretical concern; it’s a performance death spiral. While you don’t need the heavy overhead of a cryptographic vs non-cryptographic hash distinction for a standard `std::unordered_map`, you still need the bits to dance. If the input bits don’t scramble thoroughly, the pattern of your data will eventually align with the pattern of your hash, and your latency-sensitive code will suddenly start behaving like it’s running on a 1990s mainframe.

The Rules That Actually Matter

  • Respect the bit-width. If you’re building a hash for a 64-bit architecture but your mixing function truncates everything to 32 bits, you aren’t writing a hash function; you’re writing a collision generator.
  • Avoid the modulo bias trap. Using `hash % table_size` is fine if your table size is a power of two and your hash is perfectly uniform, but if it isn’t, you’re going to cluster your data in ways that kill your cache locality.
  • Don’t let the compiler optimize your entropy away. If you use a constant seed that the compiler can see through, it might pre-calculate parts of your hash at compile time, effectively turning your “randomized” hash into a predictable, deterministic mess.
  • Prioritize bit-level diffusion over arithmetic complexity. I’ve seen people try to use heavy floating-point math to “smear” bits, but a few well-placed bitwise rotations and XORs will do more for your distribution while being significantly easier on the pipeline.
  • Test against the worst case, not the average. A hash function that works on a random dataset is easy to write; a hash function that doesn’t collapse when someone feeds it a sequence of incrementing integers is what you actually need in production.

The Cost of Ignoring the Details

A hash function that isn’t uniform isn’t just “slow”—it’s a ticking time bomb for your hash table’s complexity, turning $O(1)$ lookups into a linear crawl when collisions spike.

If your function doesn’t exhibit a strong avalanche effect, you’re essentially leaving your data’s structure exposed to patterns that will eventually collapse your performance.

Don’t trust a “good enough” implementation; if you don’t respect how bits propagate through your hash, you’ll be the one debugging the latency spikes in production.

Stop Guessing, Start Testing

At the end of the day, a hash function isn’t just a mathematical abstraction; it is a piece of critical infrastructure that dictates your system’s latency profile. If you ignore uniform distribution, you aren’t just getting slightly slower lookups—you are inviting catastrophic collision clusters that turn your O(1) complexity into a linear crawl. If you ignore the avalanche effect, you are essentially building a system that is vulnerable to patterns in your input data that the compiler and the CPU will eventually exploit in ways you didn’t intend. You cannot simply write a function and hope for the best; you have to verify the distribution against your actual data profiles.

Writing high-performance C++ means moving past the “it works on my machine” phase and into the realm of understanding how your code interacts with the underlying hardware and the logic of the language. Don’t settle for a black-box implementation from a library if you don’t understand its failure modes. Take the time to profile, to test for bit-level diffusion, and to respect the mechanics of the machine. When you finally stop treating your hash functions as magic spells and start treating them as precision tools, you’ll stop shipping bugs and start shipping systems that actually scale.

About Ruaridh Kensington-Oyelaran

C++ rewards people who know what the compiler is allowed to do. I write about the rules that bite, the ones nobody mentions until you have already shipped the bug.

More From Author

Best package managers for C++ development.

C++ Finally Has Package Managers Worth Using

Deadlock and how to avoid it guide.

Deadlock Is a Design Problem, Not a Timing Problem