Skip to content

Commit d5da4e2

Browse files
Merge pull request #43 from marius-bughiu/feature/unsigned-hashers
Add UInt32Hasher and UInt64Hasher
2 parents 5694048 + d16dd13 commit d5da4e2

6 files changed

Lines changed: 188 additions & 1 deletion

File tree

CHANGELOG.md

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

77
### Added
88

9+
- `UInt32Hasher` in `Celerity.Hashing` — Wang/Jenkins-style bit-mixer for `uint` keys. Struct hasher, `AggressiveInlining`. Counterpart to `Int32WangNaiveHasher`.
10+
- `UInt64Hasher` in `Celerity.Hashing` — Murmur3 `fmix64` finalizer for `ulong` keys. Struct hasher, `AggressiveInlining`. Counterpart to `Int64Murmur3Hasher`.
11+
- `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.
912
- `DefaultHasher<T>` in `Celerity.Hashing` — a general-purpose `IHashProvider<T>` that delegates to `EqualityComparer<T>.Default.GetHashCode()`. Use it when no specialized hasher exists for a type (e.g. `Guid`, custom structs, or reference types). It is a struct, so the JIT devirtualizes the outer call on the probe path; the inner `EqualityComparer<T>` dispatch is unavoidable but acceptable for non-hot-path types.
1013
- XML doc comments added to `IHashProvider<T>`, `Int32WangNaiveHasher`, `Int64Murmur3Hasher`, and `StringFnV1AHasher`. All public hasher types now carry full XML documentation.
1114
- `DefaultHasherTests` — verifies BCL contract equivalence for int, string, and Guid keys; determinism across calls and struct instances; and integration tests confirming `DefaultHasher<T>` satisfies the hasher constraints on `CeleritySet<T,THasher>`, `IntSet<THasher>`, and `CelerityDictionary<TKey,TValue,THasher>`.

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

5555
### Infrastructure
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using Celerity.Hashing;
2+
3+
namespace Celerity.Tests.Hashing;
4+
5+
public class UInt32HasherTests
6+
{
7+
private readonly UInt32Hasher _hasher = new UInt32Hasher();
8+
9+
[Theory]
10+
[InlineData(0u, 0)]
11+
[InlineData(1u, 1)]
12+
[InlineData(16u, 16)]
13+
[InlineData(65536u, 65537)]
14+
[InlineData(uint.MaxValue, -65536)] // 0xFFFFFFFF ^ 0x0000FFFF = 0xFFFF0000
15+
[InlineData(0x80000000u, -2147450880)] // 0x80000000 ^ 0x00008000 = 0x80008000
16+
public void Hash_ReturnsExpected(uint input, int expected)
17+
{
18+
int result = _hasher.Hash(input);
19+
Assert.Equal(expected, result);
20+
}
21+
22+
[Fact]
23+
public void Hash_IsDeterministic()
24+
{
25+
uint value = 12345u;
26+
int result1 = _hasher.Hash(value);
27+
int result2 = _hasher.Hash(value);
28+
Assert.Equal(result1, result2);
29+
}
30+
31+
[Fact]
32+
public void Hash_DoesNotThrow()
33+
{
34+
uint[] testValues =
35+
{
36+
0u,
37+
1u,
38+
uint.MaxValue,
39+
0x7FFFFFFFu,
40+
0x80000000u,
41+
123456789u,
42+
987654321u,
43+
};
44+
45+
foreach (uint val in testValues)
46+
{
47+
var exception = Record.Exception(() => _hasher.Hash(val));
48+
Assert.Null(exception);
49+
}
50+
}
51+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using Celerity.Hashing;
2+
3+
namespace Celerity.Tests.Hashing;
4+
5+
public class UInt64HasherTests
6+
{
7+
private readonly UInt64Hasher _hasher = new UInt64Hasher();
8+
9+
[Fact]
10+
public void Hash_Zero_ReturnsZero()
11+
{
12+
// Murmur3 fmix64 maps 0 -> 0 (each stage is a no-op on the zero state).
13+
Assert.Equal(0, _hasher.Hash(0UL));
14+
}
15+
16+
[Fact]
17+
public void Hash_IsDeterministic()
18+
{
19+
ulong value = 0xDEADBEEFCAFEBABEUL;
20+
int result1 = _hasher.Hash(value);
21+
int result2 = _hasher.Hash(value);
22+
Assert.Equal(result1, result2);
23+
}
24+
25+
[Fact]
26+
public void Hash_DistinctInputs_ProduceDistinctResultsForSmallRange()
27+
{
28+
// Murmur3 fmix64 is a bijection on 64 bits; truncating to 32 bits on a
29+
// small sequential range should still produce distinct hashes with
30+
// overwhelming probability. A collision here would indicate a broken
31+
// mixer rather than an expected birthday-paradox event.
32+
var seen = new HashSet<int>();
33+
for (ulong i = 0; i < 1000; i++)
34+
{
35+
Assert.True(seen.Add(_hasher.Hash(i)),
36+
$"Unexpected collision at input {i}.");
37+
}
38+
}
39+
40+
[Fact]
41+
public void Hash_HighBits_InfluenceResult()
42+
{
43+
// Avalanche check: two inputs that differ only in their top bit
44+
// should produce different 32-bit hashes.
45+
int low = _hasher.Hash(1UL);
46+
int high = _hasher.Hash(1UL | (1UL << 63));
47+
Assert.NotEqual(low, high);
48+
}
49+
50+
[Fact]
51+
public void Hash_DoesNotThrow()
52+
{
53+
ulong[] testValues =
54+
{
55+
0UL,
56+
1UL,
57+
ulong.MaxValue,
58+
0x7FFFFFFFFFFFFFFFUL,
59+
0x8000000000000000UL,
60+
0xDEADBEEFCAFEBABEUL,
61+
};
62+
63+
foreach (ulong val in testValues)
64+
{
65+
var exception = Record.Exception(() => _hasher.Hash(val));
66+
Assert.Null(exception);
67+
}
68+
}
69+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace Celerity.Hashing;
4+
5+
/// <summary>
6+
/// A fast hash provider for <see cref="uint"/> keys using a Wang/Jenkins-style
7+
/// integer bit-mixer.
8+
/// </summary>
9+
/// <remarks>
10+
/// This is the <see cref="uint"/> counterpart to <see cref="Int32WangNaiveHasher"/>.
11+
/// It folds the high bits of the value into the low bits (<c>key ^ (key &gt;&gt; 16)</c>),
12+
/// then reinterprets the 32-bit result as a signed integer. Prefer this when
13+
/// key distribution is already reasonably uniform and latency matters more than
14+
/// collision resistance; use a full Murmur3 finalizer for clustered or
15+
/// adversarial inputs.
16+
/// </remarks>
17+
public struct UInt32Hasher : IHashProvider<uint>
18+
{
19+
/// <inheritdoc/>
20+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
21+
public int Hash(uint key) => (int)(key ^ (key >> 16));
22+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace Celerity.Hashing;
4+
5+
/// <summary>
6+
/// A high-quality hash provider for <see cref="ulong"/> keys using the
7+
/// Murmur3 64-bit finalizer ("fmix64").
8+
/// </summary>
9+
/// <remarks>
10+
/// This is the <see cref="ulong"/> counterpart to <see cref="Int64Murmur3Hasher"/>.
11+
/// Every input bit affects every output bit, making it a good choice for
12+
/// clustered or adversarial key distributions. The 64-bit result is truncated
13+
/// to 32 bits by taking the lower half and reinterpreting it as a signed int.
14+
/// </remarks>
15+
public struct UInt64Hasher : IHashProvider<ulong>
16+
{
17+
private const ulong C1 = 0xff51afd7ed558ccdUL;
18+
private const ulong C2 = 0xc4ceb9fe1a85ec53UL;
19+
20+
/// <inheritdoc/>
21+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
22+
public int Hash(ulong key)
23+
{
24+
// XOR with its shifted self.
25+
key ^= key >> 33;
26+
27+
// Multiply by a large odd constant.
28+
key *= C1;
29+
30+
// XOR again with its shifted self.
31+
key ^= key >> 33;
32+
33+
// Multiply by another large odd constant.
34+
key *= C2;
35+
36+
// Final XOR.
37+
key ^= key >> 33;
38+
39+
// Take the lower 32 bits as the final hash value.
40+
return (int)key;
41+
}
42+
}

0 commit comments

Comments
 (0)