Skip to content

Commit 9d9c788

Browse files
marius-bughiuclaude
andcommitted
Add Int32Murmur3Hasher
A higher-quality avalanche counterpart to Int32WangNaiveHasher, using the canonical Murmur3 fmix32 finalizer. Struct hasher with AggressiveInlining so calls devirtualize on the probe path through the struct-constrained generic parameter. Tests cover exact reference values (regenerated via Python against the canonical algorithm), the 0 -> 0 identity, determinism, sign-bit avalanche, a fixed-point sanity check, and a 1000-value distinctness sweep. Refs #24 in ISSUES.md. Closes the Int32Murmur3Hasher item on milestone 1.1.0 in ROADMAP.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d5da4e2 commit 9d9c788

4 files changed

Lines changed: 157 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ All notable changes to Celerity are documented here. This project follows [Keep
66

77
### Added
88

9+
- `Int32Murmur3Hasher` in `Celerity.Hashing` — Murmur3 `fmix32` finalizer for `int` keys. Struct hasher, `AggressiveInlining`. Higher-quality avalanche counterpart to `Int32WangNaiveHasher`; use this when keys may be clustered or adversarial.
10+
- `Int32Murmur3HasherTests` — exact-value cases against the canonical Murmur3 fmix32 reference (including `0`, `1`, `-1`, `int.MinValue`, `int.MaxValue`), determinism, avalanche on the sign bit, a fixed-point sanity check guarding against the finalizer being bypassed, and a 1000-value distinctness sweep.
911
- `UInt32Hasher` in `Celerity.Hashing` — Wang/Jenkins-style bit-mixer for `uint` keys. Struct hasher, `AggressiveInlining`. Counterpart to `Int32WangNaiveHasher`.
1012
- `UInt64Hasher` in `Celerity.Hashing` — Murmur3 `fmix64` finalizer for `ulong` keys. Struct hasher, `AggressiveInlining`. Counterpart to `Int64Murmur3Hasher`.
1113
- `UInt32HasherTests` and `UInt64HasherTests` — exact-value cases (including values crossing the sign bit), determinism, avalanche on the top bit, and a 1000-value distinctness sweep for the 64-bit mixer.

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ The next release rounds out the `Celerity.Collections` package with missing coll
4949

5050
### Hashers
5151

52-
- Add `Int32Murmur3Hasher`, `Int64WangHasher`, `GuidHasher`, `UInt32Hasher`, `UInt64Hasher`. (#24) — `UInt32Hasher` and `UInt64Hasher` `done`; the others still `planned`.
52+
- Add `Int32Murmur3Hasher`, `Int64WangHasher`, `GuidHasher`, `UInt32Hasher`, `UInt64Hasher`. (#24) — `Int32Murmur3Hasher`, `UInt32Hasher`, and `UInt64Hasher` `done`; `Int64WangHasher` and `GuidHasher` still `planned`.
5353
- Add `DefaultHasher<T>` fallback to `EqualityComparer<T>.Default.GetHashCode()`.
5454

5555
### Infrastructure
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using Celerity.Hashing;
2+
3+
namespace Celerity.Tests.Hashing;
4+
5+
public class Int32Murmur3HasherTests
6+
{
7+
private readonly Int32Murmur3Hasher _hasher = new Int32Murmur3Hasher();
8+
9+
// Reference values produced by the canonical MurmurHash3 fmix32 finalizer
10+
// (k ^= k >> 16; k *= 0x85ebca6b; k ^= k >> 13; k *= 0xc2b2ae35; k ^= k >> 16).
11+
// Exact values guard against accidental constant or shift edits.
12+
[Theory]
13+
[InlineData(0, 0)]
14+
[InlineData(1, 1364076727)] // 0x514E28B7
15+
[InlineData(-1, -2114883783)] // 0x81F16F39 (input 0xFFFFFFFF)
16+
[InlineData(int.MinValue, 1832674720)] // 0x6D3C65A0 (input 0x80000000)
17+
[InlineData(int.MaxValue, -104067416)] // 0xF9CC0EA8 (input 0x7FFFFFFF)
18+
[InlineData(123456789, -1168058214)] // 0xBA60D89A
19+
public void Hash_ReturnsExpected(int input, int expected)
20+
{
21+
Assert.Equal(expected, _hasher.Hash(input));
22+
}
23+
24+
[Fact]
25+
public void Hash_Zero_ReturnsZero()
26+
{
27+
// Murmur3 fmix32 maps 0 -> 0 (each stage is a no-op on the zero state).
28+
Assert.Equal(0, _hasher.Hash(0));
29+
}
30+
31+
[Fact]
32+
public void Hash_IsDeterministic()
33+
{
34+
int value = unchecked((int)0xDEADBEEF);
35+
int result1 = _hasher.Hash(value);
36+
int result2 = _hasher.Hash(value);
37+
Assert.Equal(result1, result2);
38+
}
39+
40+
[Fact]
41+
public void Hash_DistinctInputs_ProduceDistinctResultsForSmallRange()
42+
{
43+
// fmix32 is a bijection on 32 bits, so a sequential sweep of 1000 inputs
44+
// must produce 1000 distinct outputs. A collision here would indicate a
45+
// broken mixer rather than an expected birthday-paradox event.
46+
var seen = new HashSet<int>();
47+
for (int i = 0; i < 1000; i++)
48+
{
49+
Assert.True(seen.Add(_hasher.Hash(i)),
50+
$"Unexpected collision at input {i}.");
51+
}
52+
}
53+
54+
[Fact]
55+
public void Hash_HighBit_InfluencesResult()
56+
{
57+
// Avalanche check: two inputs that differ only in the sign bit should
58+
// produce very different 32-bit hashes. The naive Wang-style mixer only
59+
// folds the top 16 bits into the bottom 16, so a single-bit flip in the
60+
// MSB barely changes the low half of the output — Murmur3 fmix32 must
61+
// do much better than that.
62+
int low = _hasher.Hash(1);
63+
int high = _hasher.Hash(unchecked((int)(1u | (1u << 31))));
64+
Assert.NotEqual(low, high);
65+
}
66+
67+
[Fact]
68+
public void Hash_SequentialInputs_AreNotIdentity()
69+
{
70+
// A good mixer maps adjacent integers to distant hashes. Regression
71+
// guard against accidentally returning the identity: hash(i) == i for
72+
// many sequential i would indicate the finalizer was bypassed.
73+
int identityMatches = 0;
74+
for (int i = 0; i < 256; i++)
75+
{
76+
if (_hasher.Hash(i) == i)
77+
{
78+
identityMatches++;
79+
}
80+
}
81+
82+
// Only i = 0 is a fixed point of fmix32 in this range; anything more
83+
// than that would indicate a broken implementation.
84+
Assert.Equal(1, identityMatches);
85+
}
86+
87+
[Fact]
88+
public void Hash_DoesNotThrow()
89+
{
90+
int[] testValues =
91+
{
92+
0,
93+
1,
94+
-1,
95+
int.MaxValue,
96+
int.MinValue,
97+
123456789,
98+
-987654321,
99+
};
100+
101+
foreach (int val in testValues)
102+
{
103+
var exception = Record.Exception(() => _hasher.Hash(val));
104+
Assert.Null(exception);
105+
}
106+
}
107+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace Celerity.Hashing;
4+
5+
/// <summary>
6+
/// A high-quality hash provider for <see cref="int"/> keys using the
7+
/// Murmur3 32-bit finalizer ("fmix32").
8+
/// </summary>
9+
/// <remarks>
10+
/// This is the same finalizer used in MurmurHash3 for 32-bit keys. Every input
11+
/// bit affects every output bit, giving excellent avalanche properties at the
12+
/// cost of a few more instructions than <see cref="Int32WangNaiveHasher"/>.
13+
/// Prefer this hasher when keys may be clustered or adversarial; prefer
14+
/// <see cref="Int32WangNaiveHasher"/> when keys are already well distributed
15+
/// and latency dominates.
16+
/// </remarks>
17+
public struct Int32Murmur3Hasher : IHashProvider<int>
18+
{
19+
private const uint C1 = 0x85ebca6bU;
20+
private const uint C2 = 0xc2b2ae35U;
21+
22+
/// <inheritdoc/>
23+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
24+
public int Hash(int key)
25+
{
26+
// Reinterpret as unsigned so the shifts are logical, not arithmetic.
27+
uint k = unchecked((uint)key);
28+
29+
// XOR with its shifted self.
30+
k ^= k >> 16;
31+
32+
// Multiply by a large odd constant.
33+
k *= C1;
34+
35+
// XOR again with its shifted self.
36+
k ^= k >> 13;
37+
38+
// Multiply by another large odd constant.
39+
k *= C2;
40+
41+
// Final XOR.
42+
k ^= k >> 16;
43+
44+
// Reinterpret the 32-bit result as a signed integer.
45+
return unchecked((int)k);
46+
}
47+
}

0 commit comments

Comments
 (0)