Skip to content
Merged
30 changes: 21 additions & 9 deletions eng/StackExchange.Redis.Build/FastHashGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using RESPite;

namespace StackExchange.Redis.Build;

Expand Down Expand Up @@ -78,7 +79,15 @@ private static string GetName(INamedTypeSymbol type)
string name = named.Name, value = "";
foreach (var attrib in named.GetAttributes())
{
if (attrib.AttributeClass?.Name == "FastHashAttribute")
if (attrib.AttributeClass is {
Name: "FastHashAttribute",
ContainingType: null,
ContainingNamespace:
{
Name: "RESPite",
ContainingNamespace.IsGlobalNamespace: true,
}
})
{
if (attrib.ConstructorArguments.Length == 1)
{
Expand Down Expand Up @@ -178,25 +187,28 @@ private void Generate(
// perform string escaping on the generated value (this includes the quotes, note)
var csValue = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(literal.Value)).ToFullString();

var hash = FastHash.Hash64(buffer.AsSpan(0, len));
var hashCS = FastHash.HashCS(buffer.AsSpan(0, len));
var hashCI = FastHash.HashCI(buffer.AsSpan(0, len));
NewLine().Append("static partial class ").Append(literal.Name);
NewLine().Append("{");
indent++;
NewLine().Append("public const int Length = ").Append(len).Append(';');
NewLine().Append("public const long Hash = ").Append(hash).Append(';');
NewLine().Append("public const long HashCS = ").Append(hashCS).Append(';');
NewLine().Append("public const long HashCI = ").Append(hashCI).Append(';');
NewLine().Append("public static ReadOnlySpan<byte> U8 => ").Append(csValue).Append("u8;");
NewLine().Append("public const string Text = ").Append(csValue).Append(';');
if (len <= 8)
if (len <= FastHash.MaxBytesHashIsEqualityCS)
{
// the hash enforces all the values
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.Payload.Length == Length;");
NewLine().Append("public static bool Is(long hash, ReadOnlySpan<byte> value) => hash == Hash & value.Length == Length;");
// the case-sensitive hash enforces all the values
NewLine().Append("public static bool IsCS(long hash, ReadOnlySpan<byte> value) => hash == HashCS & value.Length == Length;");
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));");
}
else
{
NewLine().Append("public static bool Is(long hash, in RawResult value) => hash == Hash && value.IsEqual(U8);");
NewLine().Append("public static bool Is(long hash, ReadOnlySpan<byte> value) => hash == Hash && value.SequenceEqual(U8);");
NewLine().Append("public static bool IsCS(long hash, ReadOnlySpan<byte> value) => hash == HashCS && value.SequenceEqual(U8);");
NewLine().Append("public static bool IsCI(long hash, ReadOnlySpan<byte> value) => (hash == HashCI & value.Length == Length) && global::RESPite.FastHash.EqualsCI(value, U8);");
}

indent--;
NewLine().Append("}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\..\src\StackExchange.Redis\FastHash.cs">
<Link>FastHash.cs</Link>
<Compile Include="..\..\src\RESPite\Shared\FastHash.cs">
<Link>Shared/FastHash.cs</Link>
</Compile>
<Compile Include="..\..\src\RESPite\Shared\Experiments.cs">
<Link>Shared/Experiments.cs</Link>
</Compile>
</ItemGroup>

Expand Down
17 changes: 17 additions & 0 deletions src/RESPite/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@
[SER004]RESPite.Buffers.CycleBuffer.UncommittedAvailable.get -> int
[SER004]RESPite.Buffers.CycleBuffer.Write(in System.Buffers.ReadOnlySequence<byte> value) -> void
[SER004]RESPite.Buffers.CycleBuffer.Write(System.ReadOnlySpan<byte> value) -> void
[SER004]RESPite.FastHash
[SER004]RESPite.FastHash.FastHash() -> void
[SER004]RESPite.FastHash.FastHash(System.ReadOnlyMemory<byte> value) -> void
[SER004]RESPite.FastHash.FastHash(System.ReadOnlySpan<byte> value) -> void
[SER004]RESPite.FastHash.IsCI(long hash, System.ReadOnlySpan<byte> value) -> bool
[SER004]RESPite.FastHash.IsCI(System.ReadOnlySpan<byte> value) -> bool
[SER004]RESPite.FastHash.IsCS(long hash, System.ReadOnlySpan<byte> value) -> bool
[SER004]RESPite.FastHash.IsCS(System.ReadOnlySpan<byte> value) -> bool
[SER004]RESPite.FastHash.Length.get -> int
[SER004]RESPite.FastHashAttribute
[SER004]RESPite.FastHashAttribute.FastHashAttribute(string! token = "") -> void
[SER004]RESPite.FastHashAttribute.Token.get -> string!
[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll<TResult>(scoped System.Span<TResult> target, RESPite.Messages.RespReader.Projection<TResult>! projection) -> void
[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
[SER004]RESPite.Messages.RespReader.AggregateEnumerator.FillAll<TState, TResult>(scoped System.Span<TResult> target, ref TState state, RESPite.Messages.RespReader.Projection<TState, TResult>! projection) -> void
Expand Down Expand Up @@ -157,6 +169,11 @@
[SER004]RESPite.Messages.RespScanState.TryRead(System.ReadOnlySpan<byte> value, out int bytesRead) -> bool
[SER004]RESPite.RespException
[SER004]RESPite.RespException.RespException(string! message) -> void
[SER004]static RESPite.FastHash.EqualsCI(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<byte> second) -> bool
[SER004]static RESPite.FastHash.EqualsCS(System.ReadOnlySpan<byte> first, System.ReadOnlySpan<byte> second) -> bool
[SER004]static RESPite.FastHash.HashCI(scoped System.ReadOnlySpan<byte> value) -> long
[SER004]static RESPite.FastHash.HashCS(scoped System.ReadOnlySpan<byte> value) -> long
[SER004]static RESPite.FastHash.HashCS(System.Buffers.ReadOnlySequence<byte> value) -> long
[SER004]static RESPite.Messages.RespFrameScanner.Default.get -> RESPite.Messages.RespFrameScanner!
[SER004]static RESPite.Messages.RespFrameScanner.Subscription.get -> RESPite.Messages.RespFrameScanner!
[SER004]virtual RESPite.Messages.RespAttributeReader<T>.Read(ref RESPite.Messages.RespReader reader, ref T value) -> void
Expand Down
1 change: 1 addition & 0 deletions src/RESPite/RESPite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

<InternalsVisibleTo Include="StackExchange.Redis" />
<InternalsVisibleTo Include="StackExchange.Redis.Tests" />
<InternalsVisibleTo Include="StackExchange.Redis.Benchmarks" />
</ItemGroup>


Expand Down
119 changes: 96 additions & 23 deletions src/StackExchange.Redis/FastHash.cs → src/RESPite/Shared/FastHash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace StackExchange.Redis;
namespace RESPite;

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

internal static class FastHash
[Experimental(Experiments.Respite, UrlFormat = Experiments.UrlFormat)]
public readonly struct FastHash
{
/* not sure we need this, but: retain for reference
private readonly long _hashCI;
private readonly long _hashCS;
private readonly ReadOnlyMemory<byte> _value;
public int Length => _value.Length;

public FastHash(ReadOnlySpan<byte> value) : this((ReadOnlyMemory<byte>)value.ToArray()) { }
public FastHash(ReadOnlyMemory<byte> value)
{
_value = value;
var span = value.Span;
_hashCI = HashCI(span);
_hashCS = HashCS(span);
}

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

public static long Hash64CI(this ReadOnlySequence<byte> value)
=> value.Hash64() & CaseMask;
public static long Hash64CI(this scoped ReadOnlySpan<byte> value)
=> value.Hash64() & CaseMask;
*/
public bool IsCS(ReadOnlySpan<byte> value) => IsCS(HashCS(value), value);

public bool IsCS(long hash, ReadOnlySpan<byte> value)
{
var len = _value.Length;
if (hash != _hashCS | (value.Length != len)) return false;
return len <= MaxBytesHashIsEqualityCS || EqualsCS(_value.Span, value);
}

public bool IsCI(ReadOnlySpan<byte> value) => IsCI(HashCI(value), value);
public bool IsCI(long hash, ReadOnlySpan<byte> value)
{
var len = _value.Length;
if (hash != _hashCI | (value.Length != len)) return false;
if (len <= MaxBytesHashIsEqualityCS && HashCS(value) == _hashCS) return true;
return EqualsCI(_value.Span, value);
}

public static long Hash64(this ReadOnlySequence<byte> value)
public static long HashCS(ReadOnlySequence<byte> value)
{
#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
var first = value.FirstSpan;
#else
var first = value.First.Span;
#endif
return first.Length >= sizeof(long) || value.IsSingleSegment
? first.Hash64() : SlowHash64(value);
return first.Length >= MaxBytesHashed || value.IsSingleSegment
? HashCS(first) : SlowHashCS(value);

static long SlowHash64(ReadOnlySequence<byte> value)
static long SlowHashCS(ReadOnlySequence<byte> value)
{
Span<byte> buffer = stackalloc byte[sizeof(long)];
if (value.Length < sizeof(long))
Span<byte> buffer = stackalloc byte[MaxBytesHashed];
var len = value.Length;
if (len <= MaxBytesHashed)
{
value.CopyTo(buffer);
buffer.Slice((int)value.Length).Clear();
buffer = buffer.Slice(0, (int)len);
}
else
{
value.Slice(0, sizeof(long)).CopyTo(buffer);
value.Slice(0, MaxBytesHashed).CopyTo(buffer);
}
return BitConverter.IsLittleEndian
? Unsafe.ReadUnaligned<long>(ref MemoryMarshal.GetReference(buffer))
: BinaryPrimitives.ReadInt64LittleEndian(buffer);
return HashCS(buffer);
}
}

public static long Hash64(this scoped ReadOnlySpan<byte> value)
internal const int MaxBytesHashIsEqualityCS = sizeof(long), MaxBytesHashed = sizeof(long);

public static bool EqualsCS(ReadOnlySpan<byte> first, ReadOnlySpan<byte> second)
{
var len = first.Length;
if (len != second.Length) return false;
// for very short values, the CS hash performs CS equality
return len <= MaxBytesHashIsEqualityCS ? HashCS(first) == HashCS(second) : first.SequenceEqual(second);
}

public static unsafe bool EqualsCI(ReadOnlySpan<byte> first, ReadOnlySpan<byte> second)
{
var len = first.Length;
if (len != second.Length) return false;
// for very short values, the CS hash performs CS equality; check that first
if (len <= MaxBytesHashIsEqualityCS && HashCS(first) == HashCS(second)) return true;

// OK, don't be clever (SIMD, etc); the purpose of FashHash is to compare RESP key tokens, which are
// typically relatively short, think 3-20 bytes. That wouldn't even touch a SIMD vector, so:
// just loop (the exact thing we'd need to do *anyway* in a SIMD implementation, to mop up the non-SIMD
// trailing bytes).
fixed (byte* firstPtr = &MemoryMarshal.GetReference(first))
{
fixed (byte* secondPtr = &MemoryMarshal.GetReference(second))
{
const int CS_MASK = ~0x20;
for (int i = 0; i < len; i++)
{
byte x = firstPtr[i];
var xCI = x & CS_MASK;
if (xCI >= 'A' & xCI <= 'Z')
{
// alpha mismatch
if (xCI != (secondPtr[i] & CS_MASK)) return false;
}
else if (x != secondPtr[i])
{
// non-alpha mismatch
return false;
}
}
return true;
}
}
}

public static long HashCI(scoped ReadOnlySpan<byte> value)
=> HashCS(value) & CaseMask;

public static long HashCS(scoped ReadOnlySpan<byte> value)
{
if (BitConverter.IsLittleEndian)
{
Expand Down
Loading
Loading