Skip to content

Commit 0ed581e

Browse files
committed
core for fast-hash
1 parent f755e7a commit 0ed581e

File tree

9 files changed

+421
-7
lines changed

9 files changed

+421
-7
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
4+
namespace StackExchange.Redis;
5+
6+
// See FastHashTests for how these are validated and enforced. When adding new values, use any
7+
// value and run the tests - this will tell you the correct value.
8+
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "To better represent the expected literals")]
9+
internal static partial class FastHash
10+
{
11+
#pragma warning disable SA1300, SA1303
12+
public static class Length4
13+
{
14+
public const long size = 1702521203;
15+
public static ReadOnlySpan<byte> size_u8 => "size"u8;
16+
}
17+
#pragma warning restore SA1300, SA1303
18+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Buffers.Binary;
4+
using System.Runtime.CompilerServices;
5+
using System.Runtime.InteropServices;
6+
7+
namespace StackExchange.Redis;
8+
9+
/// <summary>
10+
/// This type is intended to provide fast hashing functions for small strings, for example well-known
11+
/// RESP literals that are usually identifiable by their length and initial bytes; it is not intended
12+
/// for general purpose hashing. All matches must also perform a sequence equality check.
13+
/// </summary>
14+
internal static partial class FastHash
15+
{
16+
public static long Hash64(this ReadOnlySequence<byte> value)
17+
{
18+
#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
19+
var first = value.FirstSpan;
20+
#else
21+
var first = value.First.Span;
22+
#endif
23+
return first.Length >= sizeof(long) || value.IsSingleSegment
24+
? first.Hash64() : SlowHash64(value);
25+
26+
static long SlowHash64(ReadOnlySequence<byte> value)
27+
{
28+
Span<byte> buffer = stackalloc byte[sizeof(long)];
29+
if (value.Length < sizeof(long))
30+
{
31+
value.CopyTo(buffer);
32+
buffer.Slice((int)value.Length).Clear();
33+
}
34+
else
35+
{
36+
value.Slice(0, sizeof(long)).CopyTo(buffer);
37+
}
38+
return BitConverter.IsLittleEndian
39+
? Unsafe.ReadUnaligned<long>(ref MemoryMarshal.GetReference(buffer))
40+
: BinaryPrimitives.ReadInt64LittleEndian(buffer);
41+
}
42+
}
43+
44+
public static long Hash64(this scoped ReadOnlySpan<byte> value)
45+
{
46+
if (BitConverter.IsLittleEndian)
47+
{
48+
ref byte data = ref MemoryMarshal.GetReference(value);
49+
return value.Length switch
50+
{
51+
0 => 0,
52+
1 => data, // 0000000A
53+
2 => Unsafe.ReadUnaligned<ushort>(ref data), // 000000BA
54+
3 => Unsafe.ReadUnaligned<ushort>(ref data) | // 000000BA
55+
(Unsafe.Add(ref data, 2) << 16), // 00000C00
56+
4 => Unsafe.ReadUnaligned<uint>(ref data), // 0000DCBA
57+
5 => Unsafe.ReadUnaligned<uint>(ref data) | // 0000DCBA
58+
((long)Unsafe.Add(ref data, 4) << 32), // 000E0000
59+
6 => Unsafe.ReadUnaligned<uint>(ref data) | // 0000DCBA
60+
((long)Unsafe.ReadUnaligned<ushort>(ref Unsafe.Add(ref data, 4)) << 32), // 00FE0000
61+
7 => Unsafe.ReadUnaligned<uint>(ref data) | // 0000DCBA
62+
((long)Unsafe.ReadUnaligned<ushort>(ref Unsafe.Add(ref data, 4)) << 32) | // 00FE0000
63+
((long)Unsafe.Add(ref data, 6) << 48), // 0G000000
64+
_ => Unsafe.ReadUnaligned<long>(ref data), // HGFEDCBA
65+
};
66+
}
67+
68+
#pragma warning disable CS0618 // Type or member is obsolete
69+
return Hash64Fallback(value);
70+
#pragma warning restore CS0618 // Type or member is obsolete
71+
}
72+
73+
[Obsolete("Only exists for benchmarks (to show that we don't need to use it) and unit tests (for correctness)")]
74+
internal static unsafe long Hash64Unsafe(scoped ReadOnlySpan<byte> value)
75+
{
76+
if (BitConverter.IsLittleEndian)
77+
{
78+
fixed (byte* ptr = &MemoryMarshal.GetReference(value))
79+
{
80+
return value.Length switch
81+
{
82+
0 => 0,
83+
1 => *ptr, // 0000000A
84+
2 => *(ushort*)ptr, // 000000BA
85+
3 => *(ushort*)ptr | // 000000BA
86+
(ptr[2] << 16), // 00000C00
87+
4 => *(int*)ptr, // 0000DCBA
88+
5 => (long)*(int*)ptr | // 0000DCBA
89+
((long)ptr[4] << 32), // 000E0000
90+
6 => (long)*(int*)ptr | // 0000DCBA
91+
((long)*(ushort*)(ptr + 4) << 32), // 00FE0000
92+
7 => (long)*(int*)ptr | // 0000DCBA
93+
((long)*(ushort*)(ptr + 4) << 32) | // 00FE0000
94+
((long)ptr[6] << 48), // 0G000000
95+
_ => *(long*)ptr, // HGFEDCBA
96+
};
97+
}
98+
}
99+
100+
return Hash64Fallback(value);
101+
}
102+
103+
[Obsolete("Only exists for unit tests and fallback")]
104+
internal static long Hash64Fallback(scoped ReadOnlySpan<byte> value)
105+
{
106+
if (value.Length < sizeof(long))
107+
{
108+
Span<byte> tmp = stackalloc byte[sizeof(long)];
109+
value.CopyTo(tmp); // ABC*****
110+
tmp.Slice(value.Length).Clear(); // ABC00000
111+
return BinaryPrimitives.ReadInt64LittleEndian(tmp); // 00000CBA
112+
}
113+
114+
return BinaryPrimitives.ReadInt64LittleEndian(value); // HGFEDCBA
115+
}
116+
}

src/StackExchange.Redis/RawResult.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,14 @@ internal bool IsEqual(in CommandBytes expected)
237237
return new CommandBytes(Payload).Equals(expected);
238238
}
239239

240-
internal unsafe bool IsEqual(byte[]? expected)
240+
internal bool IsEqual(byte[]? expected)
241241
{
242242
if (expected == null) throw new ArgumentNullException(nameof(expected));
243+
return IsEqual(new ReadOnlySpan<byte>(expected));
244+
}
243245

246+
internal bool IsEqual(ReadOnlySpan<byte> expected)
247+
{
244248
var rangeToCheck = Payload;
245249

246250
if (expected.Length != rangeToCheck.Length) return false;
@@ -250,7 +254,7 @@ internal unsafe bool IsEqual(byte[]? expected)
250254
foreach (var segment in rangeToCheck)
251255
{
252256
var from = segment.Span;
253-
var to = new Span<byte>(expected, offset, from.Length);
257+
var to = expected.Slice(offset, from.Length);
254258
if (!from.SequenceEqual(to)) return false;
255259

256260
offset += from.Length;

src/StackExchange.Redis/ResultProcessor.cs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,8 +1900,51 @@ protected override bool TryReadOne(in RawResult result, out RedisValue value)
19001900

19011901
private sealed class VectorSetInfoProcessor : ResultProcessor<VectorSetInfo?>
19021902
{
1903-
protected override bool
1904-
SetResultCore(PhysicalConnection connection, Message message, in RawResult result) => false;
1903+
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
1904+
{
1905+
if (result.Resp2TypeArray == ResultType.Array)
1906+
{
1907+
if (result.IsNull)
1908+
{
1909+
SetResult(message, null);
1910+
return true;
1911+
}
1912+
var quantType = VectorQuantizationType.Unknown;
1913+
int vectorDim = 0, maxLevel = 0;
1914+
long size = 0, vectorSetUid = 0, hnswMaxNodeUid = 0;
1915+
var iter = result.GetItems().GetEnumerator();
1916+
while (iter.MoveNext())
1917+
{
1918+
var key = iter.Current;
1919+
if (!iter.MoveNext()) break;
1920+
var value = iter.Current;
1921+
1922+
var len = key.Payload.Length;
1923+
switch (len)
1924+
{
1925+
// case 10 when key.IsEqual("quant-type"u8):
1926+
// quantType = value.AsRedisValue() switch
1927+
// {
1928+
// "NOQUANT" => VectorQuantizationType.None,
1929+
// "BIN" => VectorQuantizationType.Binary,
1930+
// "INT8" => VectorQuantizationType.Int8,
1931+
// _ => VectorQuantizationType.Unknown,
1932+
// };
1933+
// break;
1934+
case 10 when key.IsEqual("vector-dim"u8) && value.TryGetInt64(out var i64):
1935+
vectorDim = checked((int)i64);
1936+
break;
1937+
case 4 when key.IsEqual("size"u8) && value.TryGetInt64(out var i64):
1938+
size = i64;
1939+
break;
1940+
}
1941+
}
1942+
1943+
SetResult(message, new VectorSetInfo(quantType, vectorDim, size, maxLevel, vectorSetUid, hnswMaxNodeUid));
1944+
return true;
1945+
}
1946+
return false;
1947+
}
19051948
}
19061949

19071950
private sealed class VectorSetSimilaritySearchProcessor : ResultProcessor<Lease<VectorSetSimilaritySearchResult>?>

src/StackExchange.Redis/VectorSetAddMessage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ private static bool CheckFp32() // check endianness with a known value
2929
internal static void SuppressFp32() => Interlocked.Increment(ref _fp32Disabled);
3030
internal static void RestoreFp32() => Interlocked.Decrement(ref _fp32Disabled);
3131
#else
32-
internal static bool UseFP32 => CanUseFP32;
32+
internal static bool UseFp32 => CanUseFp32;
3333
internal static void SuppressFp32() { }
3434
internal static void RestoreFp32() { }
3535
#endif
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
using BenchmarkDotNet.Attributes;
6+
7+
namespace StackExchange.Redis.Benchmarks;
8+
9+
[Config(typeof(CustomConfig))]
10+
public class FastHashBenchmarks
11+
{
12+
private const string SharedString = "some-typical-data-for-comparisons";
13+
private static readonly byte[] SharedUtf8;
14+
private static readonly ReadOnlySequence<byte> SharedMultiSegment;
15+
16+
static FastHashBenchmarks()
17+
{
18+
SharedUtf8 = Encoding.UTF8.GetBytes(SharedString);
19+
20+
var first = new Segment(SharedUtf8.AsMemory(0, 1), null);
21+
var second = new Segment(SharedUtf8.AsMemory(1), first);
22+
SharedMultiSegment = new ReadOnlySequence<byte>(first, 0, second, second.Memory.Length);
23+
}
24+
25+
private sealed class Segment : ReadOnlySequenceSegment<byte>
26+
{
27+
public Segment(ReadOnlyMemory<byte> memory, Segment? previous)
28+
{
29+
Memory = memory;
30+
if (previous is { })
31+
{
32+
RunningIndex = previous.RunningIndex + previous.Memory.Length;
33+
previous.Next = this;
34+
}
35+
}
36+
}
37+
38+
private string _sourceString = SharedString;
39+
private ReadOnlyMemory<byte> _sourceBytes = SharedUtf8;
40+
private ReadOnlySequence<byte> _sourceMultiSegmentBytes = SharedMultiSegment;
41+
private ReadOnlySequence<byte> SingleSegmentBytes => new(_sourceBytes);
42+
43+
[GlobalSetup]
44+
public void Setup()
45+
{
46+
_sourceString = SharedString.Substring(0, Size);
47+
_sourceBytes = SharedUtf8.AsMemory(0, Size);
48+
_sourceMultiSegmentBytes = SharedMultiSegment.Slice(0, Size);
49+
50+
#pragma warning disable CS0618 // Type or member is obsolete
51+
var bytes = _sourceBytes.Span;
52+
var expected = FastHash.Hash64Fallback(bytes);
53+
54+
Assert(bytes.Hash64(), nameof(FastHash.Hash64));
55+
Assert(FastHash.Hash64Unsafe(bytes), nameof(FastHash.Hash64Unsafe));
56+
#pragma warning restore CS0618 // Type or member is obsolete
57+
Assert(SingleSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (single segment)");
58+
Assert(_sourceMultiSegmentBytes.Hash64(), nameof(FastHash.Hash64) + " (multi segment)");
59+
60+
void Assert(long actual, string name)
61+
{
62+
if (actual != expected)
63+
{
64+
throw new InvalidOperationException($"Hash mismatch for {name}, {expected} != {actual}");
65+
}
66+
}
67+
}
68+
69+
[ParamsSource(nameof(Sizes))]
70+
public int Size { get; set; } = 7;
71+
72+
public IEnumerable<int> Sizes => [0, 1, 2, 3, 4, 5, 6, 7, 8, 16];
73+
74+
private const int OperationsPerInvoke = 1024;
75+
76+
[Benchmark(OperationsPerInvoke = OperationsPerInvoke, Baseline = true)]
77+
public void String()
78+
{
79+
var val = _sourceString;
80+
for (int i = 0; i < OperationsPerInvoke; i++)
81+
{
82+
_ = val.GetHashCode();
83+
}
84+
}
85+
86+
[Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
87+
public void Hash64()
88+
{
89+
var val = _sourceBytes.Span;
90+
for (int i = 0; i < OperationsPerInvoke; i++)
91+
{
92+
_ = val.Hash64();
93+
}
94+
}
95+
96+
[Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
97+
public void Hash64Unsafe()
98+
{
99+
var val = _sourceBytes.Span;
100+
for (int i = 0; i < OperationsPerInvoke; i++)
101+
{
102+
#pragma warning disable CS0618 // Type or member is obsolete
103+
_ = FastHash.Hash64Unsafe(val);
104+
#pragma warning restore CS0618 // Type or member is obsolete
105+
}
106+
}
107+
108+
[Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
109+
public void Hash64Fallback()
110+
{
111+
var val = _sourceBytes.Span;
112+
for (int i = 0; i < OperationsPerInvoke; i++)
113+
{
114+
#pragma warning disable CS0618 // Type or member is obsolete
115+
_ = FastHash.Hash64Fallback(val);
116+
#pragma warning restore CS0618 // Type or member is obsolete
117+
}
118+
}
119+
120+
[Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
121+
public void Hash64_SingleSegment()
122+
{
123+
var val = SingleSegmentBytes;
124+
for (int i = 0; i < OperationsPerInvoke; i++)
125+
{
126+
_ = val.Hash64();
127+
}
128+
}
129+
130+
[Benchmark(OperationsPerInvoke = OperationsPerInvoke)]
131+
public void Hash64_MultiSegment()
132+
{
133+
var val = _sourceMultiSegmentBytes;
134+
for (int i = 0; i < OperationsPerInvoke; i++)
135+
{
136+
_ = val.Hash64();
137+
}
138+
}
139+
}
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1-
using System.Reflection;
1+
using System;
2+
using System.Reflection;
23
using BenchmarkDotNet.Running;
34

45
namespace StackExchange.Redis.Benchmarks
56
{
67
internal static class Program
78
{
8-
private static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args);
9+
private static void Main(string[] args)
10+
{
11+
#if DEBUG
12+
var obj = new FastHashBenchmarks();
13+
foreach (var size in obj.Sizes)
14+
{
15+
Console.WriteLine($"Size: {size}");
16+
obj.Size = size;
17+
obj.Setup();
18+
obj.Hash64();
19+
}
20+
#else
21+
BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args);
22+
#endif
23+
}
924
}
1025
}

tests/StackExchange.Redis.Benchmarks/StackExchange.Redis.Benchmarks.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<Configuration>Release</Configuration>
66
<OutputType>Exe</OutputType>
77
<SignAssembly>true</SignAssembly>
8+
<Nullable>enable</Nullable>
89
</PropertyGroup>
910

1011
<ItemGroup>

0 commit comments

Comments
 (0)