Skip to content

No identity hasher for uint / ulong — the zero-work tier is unreachable from a hasher-parameterized collection #357

Description

@marius-bughiu

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

  • Both types ship, mirroring Int32IdentityHasher / Int64IdentityHasher in shape and docs.
  • Dedicated tests mirroring Int32IdentityHasherTests / Int64IdentityHasherTests.
  • IntegerHasherFamilyNamingTests gains identity to its tier list, so a future width cannot ship without it.
  • _Identity arms in IntegerHasherBenchmark's UInt32 / UInt64 categories, alongside the existing signed ones.
  • docs/api/hashing.md sections, the "choosing a hasher" table rows, and the README hasher table.
  • The ROADMAP note added in refactor(hashing): name the algorithm in the unsigned hashers #355 flips from "open gap" to 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions