Observation
The zero-work identity tier exists only for the signed integer widths:
| Width |
Identity hasher |
int |
Int32IdentityHasher (Hash(key) => key) |
long |
Int64IdentityHasher (Hash(key) => (int)key) |
uint |
— |
ulong |
— |
Every other tier of the ladder — *WangNaiveHasher, *WangHasher, *Murmur3Hasher — is now present in all four widths (#297). Identity is the one hole.
Why the obvious workaround does not exist
It is tempting to say an unsigned caller reaches the floor with a cast: new Int32IdentityHasher().Hash((int)u). That is only true for a direct Hash call, which is not how the hashers are consumed. The collections take the hasher as a type parameter and invoke it internally:
public class CelerityDictionary<TKey, TValue, THasher>
where THasher : struct, IHashProvider<TKey> // CelerityDictionary.cs:21
For TKey = uint, THasher must be an IHashProvider<uint>. Int32IdentityHasher : IHashProvider<int> does not satisfy that constraint, and there is no call site for the caller to insert a cast into — the collection does the hashing. So today a uint- or ulong-keyed Celerity collection cannot be given a zero-work hasher at all; the cheapest reachable option is the XOR-fold. The same applies to every hasher-parameterized type: the sets, the frozen collections and the sketches.
Why this matters
Milestone 1.6.0 shipped the identity hashers specifically to make the "skip mixing when your keys are already uniform" advice actionable, and docs/api/hashing.md states the rule for all integer keys — uniform/trusted keys → skip mixing; clustered/adversarial keys → mix. For uint / ulong keys that advice currently has nothing to point at. Dense sequential unsigned IDs are not an exotic key shape.
Proposal
Add UInt32IdentityHasher : IHashProvider<uint> (Hash(key) => (int)key) and UInt64IdentityHasher : IHashProvider<ulong> (Hash(key) => (int)key), mirroring the signed pair exactly, including the documented caveat that the 64-bit one ignores the upper 32 bits.
Worth deciding during triage, since it is the reason the gap was not closed alongside #297: this is a two-line pair of types, which is close to the "yet another hasher variant" shape the project deliberately resists. The argument that it is not filler is that it completes an existing ladder against a documented decision rule, rather than adding a new algorithm — and that the ladder is currently unreachable from the collections for half the integer widths.
Acceptance
Found during the review of #355. The first draft of that PR asserted the opposite — that an unsigned identity hasher "adds nothing" — and pinned it with a negative test. The review caught that the premise ignores the generic constraint; the assertion and the claim were removed and the real gap filed here instead of being frozen out by a naming change.
Observation
The zero-work identity tier exists only for the signed integer widths:
intInt32IdentityHasher(Hash(key) => key)longInt64IdentityHasher(Hash(key) => (int)key)uintulongEvery other tier of the ladder —
*WangNaiveHasher,*WangHasher,*Murmur3Hasher— is now present in all four widths (#297). Identity is the one hole.Why the obvious workaround does not exist
It is tempting to say an unsigned caller reaches the floor with a cast:
new Int32IdentityHasher().Hash((int)u). That is only true for a directHashcall, which is not how the hashers are consumed. The collections take the hasher as a type parameter and invoke it internally:For
TKey = uint,THashermust be anIHashProvider<uint>.Int32IdentityHasher : IHashProvider<int>does not satisfy that constraint, and there is no call site for the caller to insert a cast into — the collection does the hashing. So today auint- orulong-keyed Celerity collection cannot be given a zero-work hasher at all; the cheapest reachable option is the XOR-fold. The same applies to every hasher-parameterized type: the sets, the frozen collections and the sketches.Why this matters
Milestone 1.6.0 shipped the identity hashers specifically to make the "skip mixing when your keys are already uniform" advice actionable, and
docs/api/hashing.mdstates the rule for all integer keys — uniform/trusted keys → skip mixing; clustered/adversarial keys → mix. Foruint/ulongkeys that advice currently has nothing to point at. Dense sequential unsigned IDs are not an exotic key shape.Proposal
Add
UInt32IdentityHasher : IHashProvider<uint>(Hash(key) => (int)key) andUInt64IdentityHasher : IHashProvider<ulong>(Hash(key) => (int)key), mirroring the signed pair exactly, including the documented caveat that the 64-bit one ignores the upper 32 bits.Worth deciding during triage, since it is the reason the gap was not closed alongside #297: this is a two-line pair of types, which is close to the "yet another hasher variant" shape the project deliberately resists. The argument that it is not filler is that it completes an existing ladder against a documented decision rule, rather than adding a new algorithm — and that the ladder is currently unreachable from the collections for half the integer widths.
Acceptance
Int32IdentityHasher/Int64IdentityHasherin shape and docs.Int32IdentityHasherTests/Int64IdentityHasherTests.IntegerHasherFamilyNamingTestsgains identity to its tier list, so a future width cannot ship without it._Identityarms inIntegerHasherBenchmark'sUInt32/UInt64categories, alongside the existing signed ones.docs/api/hashing.mdsections, the "choosing a hasher" table rows, and the README hasher table.done.Found during the review of #355. The first draft of that PR asserted the opposite — that an unsigned identity hasher "adds nothing" — and pinned it with a negative test. The review caught that the premise ignores the generic constraint; the assertion and the claim were removed and the real gap filed here instead of being frozen out by a naming change.