Commit e48955c6 authored by Marc Gravell's avatar Marc Gravell

more efficient CommandBytes; fixed size, don't store hashcode, use ulong for all internals

parent 20fcc512
using System; using System;
using System.Buffers;
using System.Text; using System.Text;
namespace StackExchange.Redis namespace StackExchange.Redis
{ {
public unsafe struct CommandBytes : IEquatable<CommandBytes> public readonly struct CommandBytes : IEquatable<CommandBytes>
{ {
public override int GetHashCode() => _hashcode; private static Encoding Encoding => Encoding.UTF8;
public override string ToString()
// Uses [n=4] x UInt64 values to store a command payload,
// allowing allocation free storage and efficient
// equality tests. If you're glancing at this and thinking
// "that's what fixed buffers are for", please see:
// https://github.com/dotnet/coreclr/issues/19149
//
// note: this tries to use case insensitive comparison
private readonly ulong _0, _1, _2;
private const int ChunkLength = 3; // must reflect qty above
public const int MaxLength = (ChunkLength * sizeof(ulong)) - 1;
public override int GetHashCode()
{
var hashCode = -1923861349;
hashCode = hashCode * -1521134295 + _0.GetHashCode();
hashCode = hashCode * -1521134295 + _1.GetHashCode();
hashCode = hashCode * -1521134295 + _2.GetHashCode();
return hashCode;
}
public override bool Equals(object obj) => obj is CommandBytes cb && Equals(cb);
public bool Equals(CommandBytes value) => _0 == value._0 && _1 == value._1 && _2 == value._2;
public static bool operator == (CommandBytes x, CommandBytes y) => x.Equals(y);
public static bool operator !=(CommandBytes x, CommandBytes y) => !x.Equals(y);
public override unsafe string ToString()
{ {
fixed (byte* ptr = _bytes) fixed (ulong* uPtr = &_0)
{ {
return Encoding.UTF8.GetString(ptr, Length); var bPtr = (byte*)uPtr;
int len = *bPtr;
return len == 0 ? "" : Encoding.GetString(bPtr + 1, *bPtr);
} }
} }
public byte this[int index] public unsafe int Length
{ {
get get
{ {
if (index < 0 || index >= Length) throw new IndexOutOfRangeException(); fixed (ulong* uPtr = &_0)
fixed (byte* ptr = _bytes)
{ {
return ptr[index]; return *(byte*)uPtr;
} }
} }
} }
public const int MaxLength = 32; // mut be multiple of 8 public unsafe byte this[int index]
public int Length { get; }
readonly int _hashcode;
fixed byte _bytes[MaxLength];
public CommandBytes(string value)
{ {
value = value.ToLowerInvariant(); get
Length = Encoding.UTF8.GetByteCount(value);
if (Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeed");
fixed (byte* bPtr = _bytes)
{ {
Clear((long*)bPtr); fixed (ulong* uPtr = &_0)
fixed (char* cPtr = value)
{ {
Encoding.UTF8.GetBytes(cPtr, value.Length, bPtr, Length); byte* bPtr = (byte*)uPtr;
int len = *bPtr;
if (index < 0 || index >= len) throw new IndexOutOfRangeException();
return bPtr[index + 1];
} }
_hashcode = GetHashCode(bPtr, Length);
} }
} }
public override bool Equals(object obj) => obj is CommandBytes cb && Equals(cb);
public bool Equals(CommandBytes value) public unsafe CommandBytes(string value)
{ {
if (_hashcode != value._hashcode || Length != value.Length) var len = Encoding.GetByteCount(value);
return false; if (len > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeed");
fixed (byte* thisB = _bytes) _0 = _1 = _2 = 0L;
fixed (ulong* uPtr = &_0)
{ {
var thisL = (long*)thisB; byte* bPtr = (byte*)uPtr;
var otherL = (long*)value._bytes; fixed (char* cPtr = value)
int chunks = (Length + 7) >> 3;
for (int i = 0; i < chunks; i++)
{ {
if (thisL[i] != otherL[i]) return false; len = Encoding.GetBytes(cPtr, value.Length, bPtr + 1, MaxLength);
} }
*bPtr = (byte)LowerCasify(len, bPtr + 1);
} }
return true;
} }
private static void Clear(long* ptr)
{ public unsafe CommandBytes(ReadOnlySpan<byte> value)
for (int i = 0; i < (MaxLength >> 3) ; i++) {
if (value.Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeed");
_0 = _1 = _2 = 0L;
fixed (ulong* uPtr = &_0)
{ {
ptr[i] = 0; byte* bPtr = (byte*)uPtr;
value.CopyTo(new Span<byte>(bPtr + 1, value.Length));
*bPtr = (byte)LowerCasify(value.Length, bPtr + 1);
} }
} }
public unsafe CommandBytes(ReadOnlySequence<byte> value)
public CommandBytes(ReadOnlySpan<byte> value)
{ {
Length = value.Length; if (value.Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeed");
if (Length > MaxLength) throw new ArgumentOutOfRangeException("Maximum command length exceeed"); int len = unchecked((int)value.Length);
fixed (byte* bPtr = _bytes) _0 = _1 = _2 = 0L;
fixed (ulong* uPtr = &_0)
{ {
Clear((long*)bPtr); byte* bPtr = (byte*)uPtr;
for (int i = 0; i < value.Length; i++) var target = new Span<byte>(bPtr + 1, len);
if (value.IsSingleSegment)
{ {
bPtr[i] = ToLowerInvariant(value[i]); value.First.Span.CopyTo(target);
} }
_hashcode = GetHashCode(bPtr, Length); else
{
foreach (var segment in value)
{
segment.Span.CopyTo(target);
target = target.Slice(segment.Length);
}
}
*bPtr = (byte)LowerCasify(len, bPtr + 1);
} }
} }
static int GetHashCode(byte* ptr, int count) private unsafe int LowerCasify(int len, byte* bPtr)
{ {
var hc = count; const ulong HighBits = 0x8080808080808080;
for (int i = 0; i < count; i++) if (((_0 | _1 | _2) & HighBits) == 0)
{
// no unicode; use ASCII bit bricks
for (int i = 0; i < len; i++)
{
*bPtr = ToLowerInvariantAscii(*bPtr++);
}
return len;
}
else
{ {
hc = (hc * -13547) + ptr[i]; return LowerCasifyUnicode(len, bPtr);
} }
return hc;
} }
static byte ToLowerInvariant(byte b) => b >= 'A' && b <= 'Z' ? (byte)(b | 32) : b;
internal byte[] ToArray() private static unsafe int LowerCasifyUnicode(int oldLen, byte* bPtr)
{
const int MaxChars = ChunkLength * sizeof(ulong); // leave rounded up; helps stackalloc
char* workspace = stackalloc char[MaxChars];
int charCount = Encoding.GetChars(bPtr, oldLen, workspace, MaxChars);
char* c = workspace;
for (int i = 0; i < charCount; i++) *c = char.ToLowerInvariant((*c++));
int newLen = Encoding.GetBytes(workspace, charCount, bPtr, MaxLength);
// don't forget to zero any shrink
for (int i = newLen; i < oldLen; i++) bPtr[i] = 0;
return newLen;
}
static byte ToLowerInvariantAscii(byte b) => b >= 'A' && b <= 'Z' ? (byte)(b | 32) : b;
internal unsafe byte[] ToArray()
{ {
fixed (byte* ptr = _bytes) fixed (ulong* uPtr = &_0)
{ {
return new Span<byte>(ptr, Length).ToArray(); byte* bPtr = (byte*)uPtr;
return new Span<byte>(bPtr + 1, *bPtr).ToArray();
} }
} }
} }
......
...@@ -59,25 +59,7 @@ internal bool TryGetCommandBytes(int i, out CommandBytes command) ...@@ -59,25 +59,7 @@ internal bool TryGetCommandBytes(int i, out CommandBytes command)
return false; return false;
} }
if (payload.Length == 0) command = payload.IsEmpty ? default : new CommandBytes(payload);
{
command = default;
}
else if (payload.IsSingleSegment)
{
command = new CommandBytes(payload.First.Span);
}
else
{
Span<byte> span = stackalloc byte[CommandBytes.MaxLength];
var sliced = span;
foreach (var segment in payload)
{
segment.Span.CopyTo(sliced);
sliced = sliced.Slice(segment.Length);
}
command = new CommandBytes(span.Slice(0, (int)payload.Length));
}
return true; return true;
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment