2026-06-16
The asker wants the fastest possible hash for a std::array<uint8_t,6> key. Their idea: pack the six bytes into a uint64_t and then hand it to std::hash<uint64_t>. Two natural ways to do the packing:
uint64_t, shift by 0/8/16/.../40, then OR them together.memcpy(&out, arr.data(), 6), relying on the compiler to fold it into a load.The interesting part is that GCC and Clang make markedly different codegen choices for these two idioms. Clang typically recognizes the shift-and-OR pattern as a "load a 6-byte little-endian value" and collapses it into a single unaligned load plus a mask. GCC often leaves the shifts as actual shifts, producing a long sequence of movzx/shl/or instructions. For the memcpy version, both compilers do well — typically a 4-byte load combined with a 2-byte load — but the exact instruction selection (movbe? two movs and a shift? a single mov + mask?) varies.
Why it's hard: 6 isn't a power of two, so there's no clean single-instruction load. The compiler must reason that reading two extra bytes past the array is illegal in general, but safe here if you mask the result. That's an analysis pass each compiler implements differently. There's also the question of whether the hash quality matters — masking off the upper 16 bits matters for correctness of equality, but if you feed the raw 64-bit register (with whatever garbage was in the high bytes) into std::hash, you get wrong answers across calls if the garbage differs.
Direction toward an answer:
-O2 and -O3.InstCombine/AggressiveInstCombine) vs GCC's bswap/load recognition pass — historically weaker for non-power-of-two widths.uint64_t x = 0; std::memcpy(&x, arr.data(), 6);. Zero-init guarantees the upper 16 bits are clean, and both compilers fold this into 1–2 loads.std::hash<uint64_t> on libstdc++ is the identity — a real avalanche step (xorshift-multiply, e.g. splitmix64 finalizer) is worth the few extra cycles.Gotchas: reading 8 bytes when only 6 are guaranteed valid is UB if the array sits at the end of a page — ASan and valgrind will (correctly) complain. Endianness flips the byte order between the two idioms, which matters if the hash is persisted or compared across machines. And on hot paths, the surrounding code (where the array lives, alignment, whether it's already in a register) often dwarfs the packing cost.
memcpy.
