Skip to content

Commit 1e02fca

Browse files
committed
Merge branch 'marc/resp-reader-csci' into marc/resp-reader-wip
2 parents 8216bfa + 6413a72 commit 1e02fca

15 files changed

Lines changed: 694 additions & 226 deletions

File tree

eng/StackExchange.Redis.Build/FastHashGenerator.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.CodeAnalysis;
66
using Microsoft.CodeAnalysis.CSharp;
77
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using RESPite;
89

910
namespace StackExchange.Redis.Build;
1011

@@ -78,7 +79,15 @@ private static string GetName(INamedTypeSymbol type)
7879
string name = named.Name, value = "";
7980
foreach (var attrib in named.GetAttributes())
8081
{
81-
if (attrib.AttributeClass?.Name == "FastHashAttribute")
82+
if (attrib.AttributeClass is {
83+
Name: "FastHashAttribute",
84+
ContainingType: null,
85+
ContainingNamespace:
86+
{
87+
Name: "RESPite",
88+
ContainingNamespace.IsGlobalNamespace: true,
89+
}
90+
})
8291
{
8392
if (attrib.ConstructorArguments.Length == 1)
8493
{
@@ -178,25 +187,28 @@ private void Generate(
178187
// perform string escaping on the generated value (this includes the quotes, note)
179188
var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString();
180189

181-
var hash = FastHash.Hash64(buffer.AsSpan(0, len));
190+
var hashCS = FastHash.HashCS(buffer.AsSpan(0, len));
191+
var hashCI = FastHash.HashCI(buffer.AsSpan(0, len));
182192
NewLine().Append("static partial class ").Append(literal.Name);
183193
NewLine().Append("{");
184194
indent++;
185195
NewLine().Append("public const int Length = ").Append(len).Append(';');
186-
NewLine().Append("public const long Hash = ").Append(hash).Append(';');
196+
NewLine().Append("public const long HashCS = ").Append(hashCS).Append(';');
197+
NewLine().Append("public const long HashCI = ").Append(hashCI).Append(';');
187198
NewLine().Append("public static ReadOnlySpan<byte> U8 => ").Append(csValue).Append("u8;");
188199
NewLine().Append("public const string Text = ").Append(csValue).Append(';');
189-
if (len <= 8)
200+
if (len <= FastHash.MaxBytesHashIsEqualityCS)
190201
{
191-
// the hash enforces all the values
192-
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;");
193-
NewLine().Append("public static bool Is(long hash, ReadOnlySpan<byte> value) => hash == Hash & value.Length == Length;");
202+
// the case-sensitive hash enforces all the values
203+
NewLine().Append("public static bool IsCS(long hash, ReadOnlySpan<byte> value) => hash == HashCS & value.Length == Length;");
204+
NewLine().Append("public static bool IsCI(long hash, ReadOnlySpan<byte> value) => (hash == HashCI & value.Length == Length) && (global::RESPite.FastHash.HashCS(value) == HashCS || global::RESPite.FastHash.EqualsCI(value, U8));");
194205
}
195206
else
196207
{
197-
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);");
198-
NewLine().Append("public static bool Is(long hash, ReadOnlySpan<byte> value) => hash == Hash && value.SequenceEqual(U8);");
208+
NewLine().Append("public static bool IsCS(long hash, ReadOnlySpan<byte> value) => hash == HashCS && value.SequenceEqual(U8);");
209+
NewLine().Append("public static bool IsCI(long hash, ReadOnlySpan<byte> value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8);");
199210
}
211+
200212
indent--;
201213
NewLine().Append("}");
202214
}

eng/StackExchange.Redis.Build/StackExchange.Redis.Build.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
</ItemGroup>
1313

1414
<ItemGroup>
15-
<Compile Include="..\..\src\StackExchange.Redis\FastHash.cs">
16-
<Link>FastHash.cs</Link>
15+
<Compile Include="..\..\src\RESPite\Shared\FastHash.cs">
16+
<Link>Shared/FastHash.cs</Link>
17+
</Compile>
18+
<Compile Include="..\..\src\RESPite\Shared\Experiments.cs">
19+
<Link>Shared/Experiments.cs</Link>
1720
</Compile>
1821
</ItemGroup>
1922

src/RESPite/PublicAPI/PublicAPI.Unshipped.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@
2020
[SER004]RESPite.Buffers.CycleBuffer.UncommittedAvailable.get -> int
2121
[SER004]RESPite.Buffers.CycleBuffer.Write(in System.Buffers.ReadOnlySequence<byte> value) -> void
2222
[SER004]RESPite.Buffers.CycleBuffer.Write(System.ReadOnlySpan<byte> value) -> void
23+
[SER004]RESPite.FastHash
24+
[SER004]RESPite.FastHash.FastHash() -> void
25+
[SER004]RESPite.FastHash.FastHash(System.ReadOnlyMemory<byte> value) -> void
26+
[SER004]RESPite.FastHash.FastHash(System.ReadOnlySpan<byte> value) -> void
27+
[SER004]RESPite.FastHash.IsCI(long hash, System.ReadOnlySpan<byte> value) -> bool
28+
[SER004]RESPite.FastHash.IsCI(System.ReadOnlySpan<byte> value) -> bool
29+
[SER004]RESPite.FastHash.IsCS(long hash, System.ReadOnlySpan<byte> value) -> bool
30+
[SER004]RESPite.FastHash.IsCS(System.ReadOnlySpan<byte> value) -> bool
31+
[SER004]RESPite.FastHashAttribute
32+
[SER004]RESPite.FastHashAttribute.FastHashAttribute(string! token = "") -> void
33+
[SER004]RESPite.FastHashAttribute.Token.get -> string!
2334
[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll<TResult>(scoped System.Span<TResult> target, RESPite.Messages.RespReader.Projection<TResult>! projection) -> void
2435
[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll<TState, TFirst, TSecond, TResult>(scoped System.Span<TResult> target, ref TState state, RESPite.Messages.RespReader.Projection<TState, TFirst>! first, RESPite.Messages.RespReader.Projection<TState, TSecond>! second, System.Func<TState, TFirst, TSecond, TResult>! combine) -> void
2536
[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll<TState, TResult>(scoped System.Span<TResult> target, ref TState state, RESPite.Messages.RespReader.Projection<TState, TResult>! projection) -> void
@@ -157,6 +168,11 @@
157168
[SER004]RESPite.Messages.RespScanState.TryRead(System.ReadOnlySpan<byte> value, out int bytesRead) -> bool
158169
[SER004]RESPite.RespException
159170
[SER004]RESPite.RespException.RespException(string! message) -> void
171+
[SER004]static RESPite.FastHash.EqualsCI(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<byte> second) -> bool
172+
[SER004]static RESPite.FastHash.EqualsCS(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<byte> second) -> bool
173+
[SER004]static RESPite.FastHash.HashCI(scoped System.ReadOnlySpan<byte> value) -> long
174+
[SER004]static RESPite.FastHash.HashCS(scoped System.ReadOnlySpan<byte> value) -> long
175+
[SER004]static RESPite.FastHash.HashCS(System.Buffers.ReadOnlySequence<byte> value) -> long
160176
[SER004]static RESPite.Messages.RespFrameScanner.Default.get -> RESPite.Messages.RespFrameScanner!
161177
[SER004]static RESPite.Messages.RespFrameScanner.Subscription.get -> RESPite.Messages.RespFrameScanner!
162178
[SER004]virtual RESPite.Messages.RespAttributeReader<T>.Read(ref RESPite.Messages.RespReader reader, ref T value) -> void

src/RESPite/RESPite.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
<InternalsVisibleTo Include="StackExchange.Redis" />
4747
<InternalsVisibleTo Include="StackExchange.Redis.Tests" />
48+
<InternalsVisibleTo Include="StackExchange.Redis.Benchmarks" />
4849
</ItemGroup>
4950

5051

Lines changed: 95 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
using System.Buffers;
33
using System.Buffers.Binary;
44
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Runtime.CompilerServices;
67
using System.Runtime.InteropServices;
78

8-
namespace StackExchange.Redis;
9+
namespace RESPite;
910

1011
/// <summary>
1112
/// This type is intended to provide fast hashing functions for small strings, for example well-known
@@ -15,54 +16,125 @@ namespace StackExchange.Redis;
1516
/// <remarks>See HastHashGenerator.md for more information and intended usage.</remarks>
1617
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
1718
[Conditional("DEBUG")] // evaporate in release
18-
internal sealed class FastHashAttribute(string token = "") : Attribute
19+
[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)]
20+
public sealed class FastHashAttribute(string token = "") : Attribute
1921
{
2022
public string Token => token;
2123
}
2224

23-
internal static class FastHash
25+
[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)]
26+
public readonly struct FastHash
2427
{
25-
/* not sure we need this, but: retain for reference
28+
private readonly long _hashCI;
29+
private readonly long _hashCS;
30+
private readonly ReadOnlyMemory<byte> _value;
31+
32+
public FastHash(ReadOnlySpan<byte> value) : this((ReadOnlyMemory<byte>)value.ToArray()) { }
33+
public FastHash(ReadOnlyMemory<byte> value)
34+
{
35+
_value = value;
36+
var span = value.Span;
37+
_hashCI = HashCI(span);
38+
_hashCS = HashCS(span);
39+
}
2640

27-
// Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves
28-
// our entropy, but is still useful when case doesn't matter.
2941
private const long CaseMask = ~0x2020202020202020;
3042

31-
public static long Hash64CI(this ReadOnlySequence<byte> value)
32-
=> value.Hash64() & CaseMask;
33-
public static long Hash64CI(this scoped ReadOnlySpan<byte> value)
34-
=> value.Hash64() & CaseMask;
35-
*/
43+
public bool IsCS(ReadOnlySpan<byte> value) => IsCS(HashCS(value), value);
44+
45+
public bool IsCS(long hash, ReadOnlySpan<byte> value)
46+
{
47+
var len = _value.Length;
48+
if (hash != _hashCS | (value.Length != len)) return false;
49+
return len <= MaxBytesHashIsEqualityCS || EqualsCS(_value.Span, value);
50+
}
51+
52+
public bool IsCI(ReadOnlySpan<byte> value) => IsCI(HashCI(value), value);
53+
public bool IsCI(long hash, ReadOnlySpan<byte> value)
54+
{
55+
var len = _value.Length;
56+
if (hash != _hashCI | (value.Length != len)) return false;
57+
if (len <= MaxBytesHashIsEqualityCS && HashCS(value) == _hashCS) return true;
58+
return EqualsCI(_value.Span, value);
59+
}
3660

37-
public static long Hash64(this ReadOnlySequence<byte> value)
61+
public static long HashCS(ReadOnlySequence<byte> value)
3862
{
3963
#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
4064
var first = value.FirstSpan;
4165
#else
4266
var first = value.First.Span;
4367
#endif
44-
return first.Length >= sizeof(long) || value.IsSingleSegment
45-
? first.Hash64() : SlowHash64(value);
68+
return first.Length >= MaxBytesHashed || value.IsSingleSegment
69+
? HashCS(first) : SlowHashCS(value);
4670

47-
static long SlowHash64(ReadOnlySequence<byte> value)
71+
static long SlowHashCS(ReadOnlySequence<byte> value)
4872
{
49-
Span<byte> buffer = stackalloc byte[sizeof(long)];
50-
if (value.Length < sizeof(long))
73+
Span<byte> buffer = stackalloc byte[MaxBytesHashed];
74+
var len = value.Length;
75+
if (len <= MaxBytesHashed)
5176
{
5277
value.CopyTo(buffer);
53-
buffer.Slice((int)value.Length).Clear();
78+
buffer = buffer.Slice(0, (int)len);
5479
}
5580
else
5681
{
57-
value.Slice(0, sizeof(long)).CopyTo(buffer);
82+
value.Slice(0, MaxBytesHashed).CopyTo(buffer);
5883
}
59-
return BitConverter.IsLittleEndian
60-
? Unsafe.ReadUnaligned<long>(ref MemoryMarshal.GetReference(buffer))
61-
: BinaryPrimitives.ReadInt64LittleEndian(buffer);
84+
return HashCS(buffer);
6285
}
6386
}
6487

65-
public static long Hash64(this scoped ReadOnlySpan<byte> value)
88+
internal const int MaxBytesHashIsEqualityCS = sizeof(long), MaxBytesHashed = sizeof(long);
89+
90+
public static bool EqualsCS(ReadOnlySpan<byte> first, ReadOnlySpan<byte> second)
91+
{
92+
var len = first.Length;
93+
if (len != second.Length) return false;
94+
// for very short values, the CS hash performs CS equality
95+
return len <= MaxBytesHashIsEqualityCS ? HashCS(first) == HashCS(second) : first.SequenceEqual(second);
96+
}
97+
98+
public static unsafe bool EqualsCI(ReadOnlySpan<byte> first, ReadOnlySpan<byte> second)
99+
{
100+
var len = first.Length;
101+
if (len != second.Length) return false;
102+
// for very short values, the CS hash performs CS equality; check that first
103+
if (len <= MaxBytesHashIsEqualityCS && HashCS(first) == HashCS(second)) return true;
104+
105+
// OK, don't be clever (SIMD, etc); the purpose of FashHash is to compare RESP key tokens, which are
106+
// typically relatively short, think 3-20 bytes. That wouldn't even touch a SIMD vector, so:
107+
// just loop (the exact thing we'd need to do *anyway* in a SIMD implementation, to mop up the non-SIMD
108+
// trailing bytes).
109+
fixed (byte* firstPtr = &MemoryMarshal.GetReference(first))
110+
{
111+
fixed (byte* secondPtr = &MemoryMarshal.GetReference(second))
112+
{
113+
const int CS_MASK = ~0x20;
114+
for (int i = 0; i < len; i++)
115+
{
116+
byte x = firstPtr[i];
117+
var xCI = x & CS_MASK;
118+
if (xCI >= 'A' & xCI <= 'Z')
119+
{
120+
// alpha mismatch
121+
if (xCI != (secondPtr[i] & CS_MASK)) return false;
122+
}
123+
else if (x != secondPtr[i])
124+
{
125+
// non-alpha mismatch
126+
return false;
127+
}
128+
}
129+
return true;
130+
}
131+
}
132+
}
133+
134+
public static long HashCI(scoped ReadOnlySpan<byte> value)
135+
=> HashCS(value) & CaseMask;
136+
137+
public static long HashCS(scoped ReadOnlySpan<byte> value)
66138
{
67139
if (BitConverter.IsLittleEndian)
68140
{

src/StackExchange.Redis/Configuration/LoggingTunnel.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Text;
1010
using System.Threading;
1111
using System.Threading.Tasks;
12+
using RESPite;
1213
using RESPite.Buffers;
1314
using RESPite.Messages;
1415
using static StackExchange.Redis.PhysicalConnection;
@@ -83,12 +84,12 @@ private static bool IsArrayOutOfBand(in RespReader source)
8384
? tmp
8485
: StackCopyLengthChecked(in reader, stackalloc byte[MAX_TYPE_LEN]);
8586

86-
var hash = span.Hash64();
87+
var hash = FastHash.HashCS(span);
8788
switch (hash)
8889
{
89-
case PushMessage.Hash when PushMessage.Is(hash, span) & len >= 3:
90-
case PushPMessage.Hash when PushPMessage.Is(hash, span) & len >= 4:
91-
case PushSMessage.Hash when PushSMessage.Is(hash, span) & len >= 3:
90+
case PushMessage.HashCS when PushMessage.IsCS(hash, span) & len >= 3:
91+
case PushPMessage.HashCS when PushPMessage.IsCS(hash, span) & len >= 4:
92+
case PushSMessage.HashCS when PushSMessage.IsCS(hash, span) & len >= 3:
9293
return true;
9394
}
9495
}

0 commit comments

Comments
 (0)