Skip to content

HashRapidV3

HashRapidV3 hashes bytes to a 64-bit value that never changes, and it is the member of the collection built for content: a file you are caching, an asset you are fingerprinting, a buffer you are comparing. Write the value into a cache header, a manifest or a network message — a different build of your program, on a different machine, years later, computes the same value from the same bytes.

const u64 identity = HashRapidV3(file_bytes);
WriteCacheHeader(out, identity); // safe: the value is part of your file format now

The name is the contract. HashRapidV3 is rapidhash V3, the full function — a published algorithm anyone can compile to reproduce every value — and it is frozen for the life of the framework: never upgraded, only ever joined by new stable hashes under their own names.

The one thing to know about it and HashRapidV3Nano

Section titled “The one thing to know about it and HashRapidV3Nano”

They are the same function up to 48 bytes and two different functions above.

Input size HashRapidV3 HashRapidV3Nano
0–48 bytes identical values identical values
49 bytes – 1 KiB different values, slightly slower different values, faster here
1 KiB and up different values, faster here (1.2–1.4× at 4 KiB) different values

So the choice is never about correctness, only about speed and about which name your format promises. The crossover is a kilobyte, measured on two compilers — not 48 bytes, which is only where the values stop agreeing. Below it the full function is still paying for a longer tail before its wider loop earns anything back.

Pick HashRapidV3 for content of a kilobyte or more — files, assets, large buffers — and HashRapidV3Nano for short keys and names. If you do not know your size distribution, either is fine: the gap is under 15% on both sides of the crossover, and below 49 bytes there is no gap at all.

Once a value is written into a format, the name that produced it is part of that format: the two are not interchangeable above 48 bytes, and no version of Basalt will ever make them so.

A frozen value is only as trustworthy as the thing that freezes it. Three mechanisms carry it:

  • The algorithm is published, not local. rapidhash V3 by Nicolas De Carli, the full function. You can compile the reference yourself and reproduce every value Basalt ever emitted — the promise is checkable from outside.
  • Frozen reference vectors pin it in the build. The corpus crosses every branch inside the algorithm — every length 0–130, the loop at one, two and three iterations, every alignment offset, degenerate contents, long inputs — each of them at seed 0 and at a non-zero seed. The table is a public contract and is never regenerated: any change to any output bit fails the build, and the only fix is to revert the change.
  • The output is a function of the bytes alone. Not of the buffer’s address, its alignment, the compiler, the optimisation level, or the machine’s byte order. The reads are defined little-endian, so x86-64 and ARM64 agree by construction.

There is deliberately no HashRapidV3(u64) and no overload set over your types. A persisted hash of a struct or an integer depends on how you serialised it — width, byte order, padding — and that layout is part of your file format, not of a hash function. Serialise to bytes first, then hash exactly the bytes you persist:

u8 header[12];
WriteLittleEndianU32(header + 0, version);
WriteLittleEndianU64(header + 4, asset_id);
const u64 identity = HashRapidV3(Str8(header, sizeof(header)));

Hashing a struct’s memory directly (Str8(reinterpret_cast<const u8*>(&s), sizeof(s))) breaks the promise from your side: padding bytes are indeterminate and field layout can change between compilers — the hash stays frozen, but the bytes you feed it do not.

  • In-memory table keys. Use HashFast: it keeps the framework free to make your tables faster later, and pinning a stable name where nothing is persisted buys you nothing.
  • Short keys you persist — anything under about a kilobyte. HashRapidV3Nano is the sibling tuned for them and is measurably faster there, and below 49 bytes it gives you the same value anyway.
  • Untrusted content. Anything an adversary can craft — see the danger note above.
  • Content too large to hold in memory at once. There is no streaming interface; the promise is over one byte string viewed whole. rapidhash has no streaming form to reproduce, so a stable hash of a stream needs a different algorithm.

Parameters, assertions and the full contract are documented in basalt/core/hash/HashRapidV3.h.

Signature Purpose
HashRapidV3(String8 bytes) The frozen 64-bit hash of the bytes. rapidhash V3, full function, unseeded.
HashRapidV3(String8 bytes, u64 seed) The same under an explicit seed; seed == 0 is the unseeded value.