Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avoid byte[] allocations when calculating cluster slot #2110

Merged
merged 15 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- Adds: Support for `GEOSEARCH` with `.GeoSearch()`/`.GeoSearchAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089))
- Adds: Support for `GEOSEARCHSTORE` with `.GeoSearchAndStore()`/`.GeoSearchAndStoreAsync()` ([#2089 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2089))
- Adds: Support for `HRANDFIELD` with `.HashRandomField()`/`.HashRandomFieldAsync()`, `.HashRandomFields()`/`.HashRandomFieldsAsync()`, and `.HashRandomFieldsWithValues()`/`.HashRandomFieldsWithValuesAsync()` ([#2090 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2090))
- Performance: avoid allocations when computing cluster hash slots ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110))

## 2.5.61

Expand Down
55 changes: 55 additions & 0 deletions src/StackExchange.Redis/RedisKey.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace StackExchange.Redis
Expand Down Expand Up @@ -297,5 +298,59 @@ internal static RedisKey WithPrefix(byte[]? prefix, RedisKey value)
/// </summary>
/// <param name="suffix">The suffix to append.</param>
public RedisKey Append(RedisKey suffix) => WithPrefix(this, suffix);


internal bool TryGetSimpleBuffer([NotNullWhen(true)] out byte[]? arr)
{
arr = KeyValue is null ? Array.Empty<byte>() : KeyValue as byte[];
return arr is not null && (KeyPrefix is null || KeyPrefix.Length == 0);
}

internal int TotalLength()
=> (KeyPrefix is null ? 0 : KeyPrefix.Length) + KeyValue switch
{
null => 0,
string s => Encoding.UTF8.GetByteCount(s),
_ => ((byte[])KeyValue).Length,
};

internal int CopyTo(Span<byte> destination)
{
int written = 0;
if (KeyPrefix is not null && KeyPrefix.Length != 0)
{
KeyPrefix.CopyTo(destination);
written += KeyPrefix.Length;
destination = destination.Slice(KeyPrefix.Length);
}
switch (KeyValue)
{
case null:
break; // nothing to do
case string s:
if (s.Length != 0)
{
#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only target netstandard2.0, simplify to #if NETCOREAPP?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted; I assume NETCOREAPP includes 5.0+?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aye, it's not netstandard but all .NET Core onwards including 5/6/7, etc.

written += Encoding.UTF8.GetBytes(s, destination);
#else
unsafe
{
fixed (byte* bPtr = destination)
fixed (char* cPtr = s)
{
written += Encoding.UTF8.GetBytes(cPtr, s.Length, bPtr, destination.Length);
}
}
#endif
}
break;
default:
var arr = (byte[])KeyValue;
arr.CopyTo(destination);
written += arr.Length;
break;
}
return written;
}
}
}
35 changes: 33 additions & 2 deletions src/StackExchange.Redis/ServerSelectionStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.Net;
using System.Threading;

Expand Down Expand Up @@ -59,13 +61,42 @@ internal sealed class ServerSelectionStrategy
/// </summary>
/// <param name="key">The <see cref="RedisKey"/> to determine a slot ID for.</param>
public int HashSlot(in RedisKey key)
=> ServerType == ServerType.Standalone || key.IsNull ? NoSlot : GetClusterSlot((byte[])key!);
{
if (ServerType == ServerType.Standalone || key.IsNull) return NoSlot;
if (key.TryGetSimpleBuffer(out var arr)) // key was constructed from a byte[]
{
return GetClusterSlot(arr);
}
else
{
var length = key.TotalLength();
if (length <= 256)
{
Span<byte> span = stackalloc byte[length];
var written = key.CopyTo(span);
Debug.Assert(written == length, "key length/write error");
return GetClusterSlot(span);
}
else
{
arr = ArrayPool<byte>.Shared.Rent(length);
var span = new Span<byte>(arr, 0, length);
var written = key.CopyTo(span);
Debug.Assert(written == length, "key length/write error");
var result = GetClusterSlot(span);
ArrayPool<byte>.Shared.Return(arr);
return result;
}
}
}

/// <summary>
/// Computes the hash-slot that would be used by the given channel.
/// </summary>
/// <param name="channel">The <see cref="RedisChannel"/> to determine a slot ID for.</param>
public int HashSlot(in RedisChannel channel)
// note that the RedisChannel->byte[] converter is always direct, so this is not an alloc
// (we deal with channels far less frequently, so pay the encoding cost up-front)
=> ServerType == ServerType.Standalone || channel.IsNull ? NoSlot : GetClusterSlot((byte[])channel!);

/// <summary>
Expand All @@ -74,7 +105,7 @@ public int HashSlot(in RedisChannel channel)
/// <remarks>
/// HASH_SLOT = CRC16(key) mod 16384
/// </remarks>
private static unsafe int GetClusterSlot(byte[] blob)
private static unsafe int GetClusterSlot(ReadOnlySpan<byte> blob)
{
unchecked
{
Expand Down
124 changes: 124 additions & 0 deletions tests/StackExchange.Redis.Tests/Keys.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -296,4 +297,127 @@ public async Task KeyRefCount()
Assert.Null(db.KeyRefCount(keyNotExists));
Assert.Null(await db.KeyRefCountAsync(keyNotExists));
}


private static void TestTotalLengthAndCopyTo(in RedisKey key, int expectedLength)
{
var length = key.TotalLength();
Assert.Equal(expectedLength, length);
var arr = ArrayPool<byte>.Shared.Rent(length + 20); // deliberately oversized
try
{
var written = key.CopyTo(arr);
Assert.Equal(length, written);

var viaCast = (byte[]?)key;
ReadOnlySpan<byte> x = viaCast, y = new ReadOnlySpan<byte>(arr, 0, length);
Assert.True(x.SequenceEqual(y));
Assert.True(key.IsNull == viaCast is null);
}
finally
{
ArrayPool<byte>.Shared.Return(arr);
}
}
[Fact]
public void NullKeySlot()
{
RedisKey key = RedisKey.Null;
Assert.True(key.TryGetSimpleBuffer(out var buffer));
Assert.Empty(buffer);
TestTotalLengthAndCopyTo(key, 0);

Assert.Equal(-1, GetHashSlot(key));
}

private static readonly byte[] KeyPrefix = Encoding.UTF8.GetBytes("abcde");

private static int GetHashSlot(in RedisKey key)
{
var strategy = new ServerSelectionStrategy(null!)
{
ServerType = ServerType.Cluster
};
return strategy.HashSlot(key);
}

[Theory]
[InlineData(false, null, -1)]
[InlineData(false, "", 0)]
[InlineData(false, "f", 3168)]
[InlineData(false, "abcde", 16097)]
[InlineData(false, "abcdef", 15101)]
[InlineData(false, "abcdeffsdkjhsdfgkjh sdkjhsdkjf hsdkjfh skudrfy7 348iu yksef78 dssdhkfh ##$OIU", 5073)]
[InlineData(false, @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras lobortis quam ac molestie ultricies. Duis maximus, nunc a auctor faucibus, risus turpis porttitor nibh, sit amet consequat lacus nibh quis nisi. Aliquam ipsum quam, dapibus ut ex eu, efficitur vestibulum dui. Sed a nibh ut felis congue tempor vel vel lectus. Phasellus a neque placerat, blandit massa sed, imperdiet urna. Praesent scelerisque lorem ipsum, non facilisis libero hendrerit quis. Nullam sit amet malesuada velit, ac lacinia lacus. Donec mollis a massa sed egestas. Suspendisse vitae augue quis erat gravida consectetur. Aenean interdum neque id lacinia eleifend.", 4954)]
[InlineData(true, null, 16097)]
[InlineData(true, "", 16097)] // note same as false/abcde
[InlineData(true, "f", 15101)] // note same as false/abcdef
[InlineData(true, "abcde", 4089)]
[InlineData(true, "abcdef", 1167)]
[InlineData(true, "abcdeffsdkjhsdfgkjh sdkjhsdkjf hsdkjfh skudrfy7 348iu yksef78 dssdhkfh ##$OIU", 10923)]
[InlineData(true, @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras lobortis quam ac molestie ultricies. Duis maximus, nunc a auctor faucibus, risus turpis porttitor nibh, sit amet consequat lacus nibh quis nisi. Aliquam ipsum quam, dapibus ut ex eu, efficitur vestibulum dui. Sed a nibh ut felis congue tempor vel vel lectus. Phasellus a neque placerat, blandit massa sed, imperdiet urna. Praesent scelerisque lorem ipsum, non facilisis libero hendrerit quis. Nullam sit amet malesuada velit, ac lacinia lacus. Donec mollis a massa sed egestas. Suspendisse vitae augue quis erat gravida consectetur. Aenean interdum neque id lacinia eleifend.", 4452)]
public void TestStringKeySlot(bool prefixed, string? s, int slot)
{
RedisKey key = prefixed ? new RedisKey(KeyPrefix, s) : s;
if (s is null && !prefixed)
{
Assert.True(key.TryGetSimpleBuffer(out var buffer));
Assert.Empty(buffer);
TestTotalLengthAndCopyTo(key, 0);
}
else
{
Assert.False(key.TryGetSimpleBuffer(out var buffer));
}
TestTotalLengthAndCopyTo(key, Encoding.UTF8.GetByteCount(s ?? "") + (prefixed ? KeyPrefix.Length : 0));

Assert.Equal(slot, GetHashSlot(key));
}

[Theory]
[InlineData(false, -1, -1)]
[InlineData(false, 0, 0)]
[InlineData(false, 1, 10242)]
[InlineData(false, 6, 10015)]
[InlineData(false, 47, 849)]
[InlineData(false, 14123, 2356)]
[InlineData(true, -1, 16097)]
[InlineData(true, 0, 16097)]
[InlineData(true, 1, 7839)]
[InlineData(true, 6, 6509)]
[InlineData(true, 47, 2217)]
[InlineData(true, 14123, 6773)]
public void TestBlobKeySlot(bool prefixed, int count, int slot)
{
byte[]? blob = null;
if (count >= 0)
{
blob = new byte[count];
new Random(count).NextBytes(blob);
for (int i = 0; i < blob.Length; i++)
{
if (blob[i] == (byte)'{') blob[i] = (byte)'!'; // avoid unexpected hash tags
}
}
RedisKey key = prefixed ? new RedisKey(KeyPrefix, blob) : blob;
if (prefixed)
{
Assert.False(key.TryGetSimpleBuffer(out _));
}
else
{
Assert.True(key.TryGetSimpleBuffer(out var buffer));
if (blob is null)
{
Assert.Empty(buffer);
}
else
{
Assert.Same(blob, buffer);
}
}
TestTotalLengthAndCopyTo(key, (blob?.Length ?? 0) + (prefixed ? KeyPrefix.Length : 0));

Assert.Equal(slot, GetHashSlot(key));
}
}