Skip to content

Commit 0260d82

Browse files
committed
Add Equals to IHasher to eliminate virtual key comparisons
1 parent 7eaa117 commit 0260d82

File tree

10 files changed

+384
-102
lines changed

10 files changed

+384
-102
lines changed

benchmarks/Faster.Map.Benchmark/GetBenchmark.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
using System.Linq;
66
using BenchmarkDotNet.Engines;
77
using Faster.Map.Benchmark.Utilities;
8-
using Faster.Map.Hasher;
98

109
namespace Faster.Map.Benchmark
1110
{
1211
[MarkdownExporterAttribute.GitHub]
1312
[DisassemblyDiagnoser]
14-
//[MemoryDiagnoser]
13+
[MemoryDiagnoser]
1514
[SimpleJob(RunStrategy.Monitoring, launchCount: 1, iterationCount: 50, warmupCount: 3)]
1615

1716
public class GetBenchmark

src/BlitzMap.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Faster.Map.Contracts;
12
using Faster.Map.Hasher;
23
using System;
34
using System.Collections;
@@ -102,8 +103,7 @@ public IEnumerator<Entry> GetEnumerator()
102103
private uint _maxCountBeforeResize;
103104
private THasher _hasher;
104105
private uint _lastBucket = INACTIVE;
105-
private EqualityComparer<TKey> _eq = EqualityComparer<TKey>.Default;
106-
106+
107107
#endregion
108108
/// <summary>
109109
/// Initializes a new instance of the <see cref="BlitzMap{TKey, TValue, THasher}"/> class
@@ -190,7 +190,7 @@ public bool Get(TKey key, out TValue value)
190190
var entry = Unsafe.Add(ref entryBase, bucket.Signature & _mask);
191191

192192
// Verify that the key matches using the equality comparer
193-
if (_eq.Equals(key, entry.Key))
193+
if (_hasher.Equals(key, entry.Key))
194194
{
195195
value = entry.Value; // Set the output value
196196
return true; // Key found, return success
@@ -239,7 +239,7 @@ public bool Contains(TKey key)
239239
// Retrieve the entry associated with the current bucket
240240
var entry = Unsafe.Add(ref entryBase, bucket.Signature & _mask);
241241
// Verify that the key matches using the equality comparer
242-
if (_eq.Equals(key, entry.Key))
242+
if (_hasher.Equals(key, entry.Key))
243243
{
244244
return true; // Key found, return success
245245
}
@@ -287,7 +287,7 @@ public bool Update(TKey key, TValue value)
287287
ref var entry = ref Unsafe.Add(ref entryBase, bucket.Signature & _mask);
288288

289289
// Verify that the key matches using the equality comparer
290-
if (_eq.Equals(key, entry.Key))
290+
if (_hasher.Equals(key, entry.Key))
291291
{
292292
entry.Value = value; // Update the value
293293
return true;
@@ -350,7 +350,7 @@ public bool Insert(TKey key, TValue value)
350350

351351
// Check if the current bucket already contains the key
352352
if (signature == (bucket.Signature & ~_mask) &&
353-
_eq.Equals(key, Unsafe.Add(ref entryBase, bucket.Signature & _mask).Key))
353+
_hasher.Equals(key, Unsafe.Add(ref entryBase, bucket.Signature & _mask).Key))
354354
{
355355
return false; // Key already exists, insertion fails
356356
}
@@ -383,7 +383,7 @@ public bool Insert(TKey key, TValue value)
383383

384384
// Check for an existing key in the chain
385385
if (signature == (bucket.Signature & ~_mask) &&
386-
_eq.Equals(key, _entries[bucket.Signature & _mask].Key))
386+
_hasher.Equals(key, _entries[bucket.Signature & _mask].Key))
387387
{
388388
return false; // Key already exists, insertion fails
389389
}
@@ -550,7 +550,7 @@ public bool InsertOrUpdate(TKey key, TValue value)
550550
else if (signature == (bucket.Signature & ~_mask))
551551
{
552552
ref var entry = ref Unsafe.Add(ref entryBase, bucket.Signature & _mask);
553-
if (_eq.Equals(key, entry.Key))
553+
if (_hasher.Equals(key, entry.Key))
554554
{
555555
entry.Value = value;
556556
return true; // Key already exists, update value
@@ -566,7 +566,7 @@ public bool InsertOrUpdate(TKey key, TValue value)
566566
if (signature == (bucket.Signature & ~_mask))
567567
{
568568
ref var entry = ref Unsafe.Add(ref entryBase, bucket.Signature & _mask);
569-
if (_eq.Equals(key, entry.Key))
569+
if (_hasher.Equals(key, entry.Key))
570570
{
571571
entry.Value = value;
572572
return true; // Key already exists, update value
@@ -624,7 +624,7 @@ public bool Remove(TKey key)
624624
uint entryIndex = bucket.Signature & _mask;
625625
ref var entry = ref Unsafe.Add(ref entryBase, entryIndex);
626626

627-
if (_eq.Equals(key, entry.Key))
627+
if (_hasher.Equals(key, entry.Key))
628628
{
629629
// --- unlink bucket from chain ---
630630
if (prevIndex == INACTIVE)

src/Contracts/IHasher.cs

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,69 @@
1-
using System.Runtime.CompilerServices;
1+
using System.Collections.Generic;
2+
using System.Runtime.CompilerServices;
23

3-
namespace Faster.Map.Contracts
4+
namespace Faster.Map.Contracts;
5+
6+
/// <summary>
7+
/// Defines a high-performance hashing and equality strategy for <c>BlitzMap</c>.
8+
///
9+
/// Implementations are intended to be <see langword="struct"/>s and supplied as generic
10+
/// type parameters in order to enable full static dispatch, aggressive inlining, and
11+
/// elimination of interface and virtual-call overhead in hot paths.
12+
///
13+
/// This interface is purpose-built for performance-critical hash tables and deliberately
14+
/// trades runtime flexibility (e.g. pluggable comparers) for compile-time specialization.
15+
///
16+
/// Unlike <see cref="IEqualityComparer{T}"/>, implementations of this interface are expected
17+
/// to be deterministic, allocation-free, and suitable for use inside tight loops.
18+
/// </summary>
19+
/// <typeparam name="TKey">
20+
/// The type of the keys being hashed and compared.
21+
/// </typeparam>
22+
public interface IHasher<TKey>
423
{
5-
public interface IHasher<TKey>
6-
{
7-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8-
uint ComputeHash(TKey key);
24+
/// <summary>
25+
/// Computes a 32-bit hash code for the specified key.
26+
///
27+
/// The returned hash must be consistent with <see cref="Equals"/>:
28+
///
29+
/// <list type="bullet">
30+
/// <item>If <c>Equals(x, y)</c> returns <see langword="true"/>, then
31+
/// <c>ComputeHash(x)</c> and <c>ComputeHash(y)</c> <b>must</b> return the same value.</item>
32+
/// <item>Unequal keys may produce identical hashes, but high-quality implementations
33+
/// should minimize collisions through good bit diffusion.</item>
34+
/// </list>
35+
///
36+
/// This method is invoked on every lookup, insertion, and removal operation and is
37+
/// therefore on the critical hot path. Implementations must be allocation-free and
38+
/// suitable for aggressive inlining.
39+
/// </summary>
40+
/// <param name="key">The key to hash. Must not be <see langword="null"/>.</param>
41+
/// <returns>
42+
/// A 32-bit unsigned hash value suitable for bucket indexing and signature derivation.
43+
/// </returns>
44+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
45+
uint ComputeHash(TKey key);
946

10-
}
47+
/// <summary>
48+
/// Determines whether two keys are considered equal.
49+
///
50+
/// Implementations must obey the standard equality contract
51+
/// (reflexive, symmetric, and transitive).
52+
///
53+
/// This method is on the hot path for all map operations and should be implemented
54+
/// using the cheapest comparison possible:
55+
///
56+
/// <list type="bullet">
57+
/// <item>For value types, this typically maps to <c>==</c>.</item>
58+
/// <item>For reference types, implementations should avoid culture-aware,
59+
/// allocation-heavy, or polymorphic comparisons unless explicitly required.</item>
60+
/// </list>
61+
/// </summary>
62+
/// <param name="x">The first key to compare.</param>
63+
/// <param name="y">The second key to compare.</param>
64+
/// <returns>
65+
/// <see langword="true"/> if the keys are equal; otherwise, <see langword="false"/>.
66+
/// </returns>
67+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
68+
bool Equals(TKey x, TKey y);
1169
}

src/DenseMap.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public DenseMap() : base(16, 0.875) { }
8888
/// <typeparam name="THasher">
8989
/// A struct implementing <see cref="Hasher.IHasher{TKey}"/> to provide an optimized hashing function.
9090
/// Using a struct-based hasher avoids virtual method calls and allows aggressive inlining.</typeparam>
91-
public class DenseMap<TKey, TValue, THasher> where THasher : struct, Hasher.IHasher<TKey>
91+
public class DenseMap<TKey, TValue, THasher> where THasher : struct, IHasher<TKey>
9292
{
9393
#region Properties
9494

@@ -210,7 +210,6 @@ public IEnumerable<TValue> Values
210210
private uint _lengthMinusOne;
211211
private readonly double _loadFactor;
212212
private readonly THasher _hasher;
213-
private readonly IEqualityComparer<TKey> _comparer;
214213

215214
#endregion
216215

@@ -275,8 +274,7 @@ public DenseMap(uint length, double loadFactor)
275274
}
276275

277276
_maxLookupsBeforeResize = (uint)(_length * _loadFactor);
278-
_comparer = EqualityComparer<TKey>.Default;
279-
277+
280278
_controlBytes = GC.AllocateArray<sbyte>((int)_length + 16);
281279
_entries = GC.AllocateArray<Entry>((int)_length + 16);
282280

@@ -349,7 +347,7 @@ public void Emplace(TKey key, TValue value)
349347
// Find the lowest set bit in `mask` (first matching position).
350348
ref var entry = ref Find(_entries, index + (uint)BitOperations.TrailingZeroCount(resultMask));
351349
// If a matching key is found, update the entry's value and return the old value.
352-
if (_comparer.Equals(entry.Key, key))
350+
if (_hasher.Equals(entry.Key, key))
353351
{
354352
entry.Value = value;
355353
return;
@@ -445,7 +443,7 @@ public bool Get(TKey key, out TValue value)
445443
// Retrieve the entry corresponding to the matched bit position within the map's entries.
446444
var entry = Find(_entries, index + (uint)bitPos);
447445
// Check if the entry's key matches the specified key using the equality comparer.
448-
if (_comparer.Equals(entry.Key, key))
446+
if (_hasher.Equals(entry.Key, key))
449447
{
450448
// If a match is found, set the output value and return true.
451449
value = entry.Value;
@@ -529,7 +527,7 @@ public ref TValue GetValueRefOrAddDefault(TKey key)
529527
// Use `bitPos` to access the corresponding entry in `_entries`.
530528
ref var entry = ref Find(_entries, index + (uint)bitPos);
531529
// If a matching key is found, update the entry's value and return the old value.
532-
if (_comparer.Equals(entry.Key, key))
530+
if (_hasher.Equals(entry.Key, key))
533531
{
534532
return ref entry.Value;
535533
}
@@ -625,7 +623,7 @@ public bool Update(TKey key, TValue value)
625623
ref var entry = ref Find(_entries, index + (uint)bitPos);
626624

627625
// Check if the current entry's key matches the specified key using the equality comparer.
628-
if (_comparer.Equals(entry.Key, key))
626+
if (_hasher.Equals(entry.Key, key))
629627
{
630628
// If a match is found, update the entry's value and return `true` to indicate success.
631629
entry.Value = value;
@@ -708,8 +706,8 @@ public bool Remove(TKey key)
708706
var i = index + (uint)bitPos;
709707

710708
// Check if the entry at the matched position has a key that equals the specified key.
711-
// Use `_comparer` to ensure accurate key comparison.
712-
if (_comparer.Equals(Find(_entries, i).Key, key))
709+
// Use `_hasher` to ensure accurate key comparison.
710+
if (_hasher.Equals(Find(_entries, i).Key, key))
713711
{
714712
// If the group that contains the entry to be removed has an empty slot (an unoccupied slot that is marked empty rather than as a tombstone), this indicates that any probe sequence for a key would terminate upon reaching that empty slot.
715713
// Since probe sequences terminate at the first empty slot they encounter, having an empty slot in the group means that removing the current entry without placing a tombstone won’t disrupt probe chains.
@@ -807,8 +805,8 @@ public bool Contains(TKey key)
807805
// Get the position of the first set bit in `mask`, indicating a potential key match.
808806
var bitPos = BitOperations.TrailingZeroCount(mask);
809807
// Check if the entry at this position has a key that matches the specified key.
810-
// Use `_comparer` to ensure accurate key comparison.
811-
if (_comparer.Equals(Find(_entries, index + (uint)bitPos).Key, key))
808+
// Use `_hasher` to ensure accurate key comparison.
809+
if (_hasher.Equals(Find(_entries, index + (uint)bitPos).Key, key))
812810
{
813811
// If a match is found, return `true` to indicate the key exists in the map.
814812
return true;

src/Faster.Map.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
<ImplicitUsings>false</ImplicitUsings>
44
<Nullable>disable</Nullable>
55
<LangVersion>preview</LangVersion>
6-
<TargetFrameworks>net9.0;net8.0;net7.0</TargetFrameworks>
6+
<TargetFrameworks>net9.0;net8.0;net7.0;</TargetFrameworks>
77
<LangVersion>Latest</LangVersion>
88
<Authors>Wiljan Ruizendaal</Authors>
99
<PackageReadmeFile>README.md</PackageReadmeFile>
1010
<CopyRight>MIT</CopyRight>
1111
<PackageReleaseNotes>
12-
minor changes and fixes
12+
Add Equals method to IHasher to eliminate virtual key comparisons
1313
</PackageReleaseNotes>
1414
<PackageProjectUrl>https://github.com/Wsm2110/Faster.Map</PackageProjectUrl>
15-
<AssemblyVersion>7.0.2</AssemblyVersion>
16-
<FileVersion>7.0.2</FileVersion>
15+
<AssemblyVersion>7.1.0</AssemblyVersion>
16+
<FileVersion>7.1.0</FileVersion>
1717
<Title>Fastest .net hashmap</Title>
18-
<Version>7.0.1</Version>
18+
<Version>7.1.0</Version>
1919
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
2020
<Description>
2121
Incredibly fast hashmap

0 commit comments

Comments
 (0)