Skip to content

Commit 536efe4

Browse files
committed
- add FastHashTests
- misc FastHash core improvements
1 parent c421ccb commit 536efe4

File tree

5 files changed

+139
-11
lines changed

5 files changed

+139
-11
lines changed

eng/StackExchange.Redis.Build/FastHashGenerator.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,17 +175,28 @@ private void Generate(
175175
}
176176
}
177177

178+
// perform string escaping on the generated value (this includes the quotes, note)
179+
var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString();
180+
178181
var hash = FastHash.Hash64(buffer.AsSpan(0, len));
179182
NewLine().Append("static partial class ").Append(literal.Name);
180183
NewLine().Append("{");
181184
indent++;
182185
NewLine().Append("public const int Length = ").Append(len).Append(';');
183186
NewLine().Append("public const long Hash = ").Append(hash).Append(';');
184-
NewLine().Append("public static ReadOnlySpan<byte> U8 => @\"")
185-
.Append(literal.Value.Replace("\"", "\"\"")).Append("\"u8;");
186-
NewLine().Append("public static string Text => @\"")
187-
.Append(literal.Value.Replace("\"", "\"\"")).Append("\";");
188-
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);");
187+
NewLine().Append("public static ReadOnlySpan<byte> U8 => ").Append(csValue).Append("u8;");
188+
NewLine().Append("public const string Text = ").Append(csValue).Append(';');
189+
if (len <= 8)
190+
{
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;");
194+
}
195+
else
196+
{
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);");
199+
}
189200
indent--;
190201
NewLine().Append("}");
191202
}

eng/StackExchange.Redis.Build/FastHashGenerator.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,17 @@ static partial class bin
2323
public const long Hash = 7235938;
2424
public static ReadOnlySpan<byte> U8 => @"bin"u8;
2525
public static string Text => @"bin";
26-
public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);
26+
public static bool Is(long hash, in RawResult value) => ...
27+
public static bool Is(long hash, in ReadOnlySpan<byte> value) => ...
2728
}
2829
static partial class f32
2930
{
3031
public const int Length = 3;
3132
public const long Hash = 3289958;
3233
public static ReadOnlySpan<byte> U8 => @"f32"u8;
33-
public static string Text => @"f32";
34-
public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);
34+
public const string Text = @"f32";
35+
public static bool Is(long hash, in RawResult value) => ...
36+
public static bool Is(long hash, in ReadOnlySpan<byte> value) => ...
3537
}
3638
```
3739

src/StackExchange.Redis/FastHash.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@ internal sealed class FastHashAttribute(string token = "") : Attribute
2222

2323
internal static class FastHash
2424
{
25+
/* not sure we need this, but: retain for reference
26+
2527
// Perform case-insensitive hash by masking (X and x differ by only 1 bit); this halves
2628
// our entropy, but is still useful when case doesn't matter.
2729
private const long CaseMask = ~0x2020202020202020;
2830
2931
public static long Hash64CI(this ReadOnlySequence<byte> value)
3032
=> value.Hash64() & CaseMask;
33+
public static long Hash64CI(this scoped ReadOnlySpan<byte> value)
34+
=> value.Hash64() & CaseMask;
35+
*/
3136

3237
public static long Hash64(this ReadOnlySequence<byte> value)
3338
{
@@ -57,9 +62,6 @@ static long SlowHash64(ReadOnlySequence<byte> value)
5762
}
5863
}
5964

60-
public static long Hash64CI(this scoped ReadOnlySpan<byte> value)
61-
=> value.Hash64() & CaseMask;
62-
6365
public static long Hash64(this scoped ReadOnlySpan<byte> value)
6466
{
6567
if (BitConverter.IsLittleEndian)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using System.Text;
4+
using Xunit;
5+
6+
#pragma warning disable CS8981, SA1134, SA1300, SA1303, SA1502 // names are weird in this test!
7+
// ReSharper disable InconsistentNaming - to better represent expected literals
8+
// ReSharper disable IdentifierTypo
9+
namespace StackExchange.Redis.Tests;
10+
11+
public partial class FastHashTests
12+
{
13+
// note: if the hashing algorithm changes, we can update the last parameter freely; it doesn't matter
14+
// what it *is* - what matters is that we can see that it has entropy between different values
15+
[Theory]
16+
[InlineData(1, a.Length, a.Text, a.Hash, 97)]
17+
[InlineData(2, ab.Length, ab.Text, ab.Hash, 25185)]
18+
[InlineData(3, abc.Length, abc.Text, abc.Hash, 6513249)]
19+
[InlineData(4, abcd.Length, abcd.Text, abcd.Hash, 1684234849)]
20+
[InlineData(5, abcde.Length, abcde.Text, abcde.Hash, 435475931745)]
21+
[InlineData(6, abcdef.Length, abcdef.Text, abcdef.Hash, 112585661964897)]
22+
[InlineData(7, abcdefg.Length, abcdefg.Text, abcdefg.Hash, 29104508263162465)]
23+
[InlineData(8, abcdefgh.Length, abcdefgh.Text, abcdefgh.Hash, 7523094288207667809)]
24+
25+
[InlineData(1, x.Length, x.Text, x.Hash, 120)]
26+
[InlineData(2, xx.Length, xx.Text, xx.Hash, 30840)]
27+
[InlineData(3, xxx.Length, xxx.Text, xxx.Hash, 7895160)]
28+
[InlineData(4, xxxx.Length, xxxx.Text, xxxx.Hash, 2021161080)]
29+
[InlineData(5, xxxxx.Length, xxxxx.Text, xxxxx.Hash, 517417236600)]
30+
[InlineData(6, xxxxxx.Length, xxxxxx.Text, xxxxxx.Hash, 132458812569720)]
31+
[InlineData(7, xxxxxxx.Length, xxxxxxx.Text, xxxxxxx.Hash, 33909456017848440)]
32+
[InlineData(8, xxxxxxxx.Length, xxxxxxxx.Text, xxxxxxxx.Hash, 8680820740569200760)]
33+
34+
[InlineData(3, .Length, .Text, .Hash, 9677543, "窓")]
35+
[InlineData(20, abcdefghijklmnopqrst.Length, abcdefghijklmnopqrst.Text, abcdefghijklmnopqrst.Hash, 7523094288207667809)]
36+
37+
// show that foo_bar is interpreted as foo-bar
38+
[InlineData(7, foo_bar.Length, foo_bar.Text, foo_bar.Hash, 32195221641981798, "foo-bar", nameof(foo_bar))]
39+
[InlineData(7, foo_bar_hyphen.Length, foo_bar_hyphen.Text, foo_bar_hyphen.Hash, 32195221641981798, "foo-bar", nameof(foo_bar_hyphen))]
40+
[InlineData(7, foo_bar_underscore.Length, foo_bar_underscore.Text, foo_bar_underscore.Hash, 32195222480842598, "foo_bar", nameof(foo_bar_underscore))]
41+
public void Validate(int expectedLength, int actualLength, string actualValue, long actualHash, long expectedHash, string? expectedValue = null, string originForDisambiguation = "")
42+
{
43+
_ = originForDisambiguation; // to allow otherwise-identical test data to coexist
44+
Assert.Equal(expectedLength, actualLength);
45+
Assert.Equal(expectedHash, actualHash);
46+
var bytes = Encoding.UTF8.GetBytes(actualValue);
47+
Assert.Equal(expectedLength, bytes.Length);
48+
Assert.Equal(expectedHash, FastHash.Hash64(bytes));
49+
#pragma warning disable CS0618 // Type or member is obsolete
50+
Assert.Equal(expectedHash, FastHash.Hash64Fallback(bytes));
51+
#pragma warning restore CS0618 // Type or member is obsolete
52+
if (expectedValue is not null)
53+
{
54+
Assert.Equal(expectedValue, actualValue);
55+
}
56+
}
57+
58+
[Fact]
59+
public void FastHashIs_Short()
60+
{
61+
ReadOnlySpan<byte> value = "abc"u8;
62+
var hash = value.Hash64();
63+
Assert.Equal(abc.Hash, hash);
64+
Assert.True(abc.Is(hash, value));
65+
66+
value = "abz"u8;
67+
hash = value.Hash64();
68+
Assert.NotEqual(abc.Hash, hash);
69+
Assert.False(abc.Is(hash, value));
70+
}
71+
72+
[Fact]
73+
public void FastHashIs_Long()
74+
{
75+
ReadOnlySpan<byte> value = "abcdefghijklmnopqrst"u8;
76+
var hash = value.Hash64();
77+
Assert.Equal(abcdefghijklmnopqrst.Hash, hash);
78+
Assert.True(abcdefghijklmnopqrst.Is(hash, value));
79+
80+
value = "abcdefghijklmnopqrsz"u8;
81+
hash = value.Hash64();
82+
Assert.Equal(abcdefghijklmnopqrst.Hash, hash); // hash collision, fine
83+
Assert.False(abcdefghijklmnopqrst.Is(hash, value));
84+
}
85+
86+
[FastHash] private static partial class a { }
87+
[FastHash] private static partial class ab { }
88+
[FastHash] private static partial class abc { }
89+
[FastHash] private static partial class abcd { }
90+
[FastHash] private static partial class abcde { }
91+
[FastHash] private static partial class abcdef { }
92+
[FastHash] private static partial class abcdefg { }
93+
[FastHash] private static partial class abcdefgh { }
94+
95+
[FastHash] private static partial class abcdefghijklmnopqrst { }
96+
97+
// show that foo_bar and foo-bar are different
98+
[FastHash] private static partial class foo_bar { }
99+
[FastHash("foo-bar")] private static partial class foo_bar_hyphen { }
100+
[FastHash("foo_bar")] private static partial class foo_bar_underscore { }
101+
102+
[FastHash] private static partial class { }
103+
104+
[FastHash] private static partial class x { }
105+
[FastHash] private static partial class xx { }
106+
[FastHash] private static partial class xxx { }
107+
[FastHash] private static partial class xxxx { }
108+
[FastHash] private static partial class xxxxx { }
109+
[FastHash] private static partial class xxxxxx { }
110+
[FastHash] private static partial class xxxxxxx { }
111+
[FastHash] private static partial class xxxxxxxx { }
112+
}

tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@
3030
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
3131
<PackageReference Include="System.IO.Compression" />
3232
<PackageReference Include="System.IO.Pipelines" />
33+
<ProjectReference Include="..\..\eng\StackExchange.Redis.Build\StackExchange.Redis.Build.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
3334
</ItemGroup>
3435
</Project>

0 commit comments

Comments
 (0)