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 all 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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)StackExchange.Redis.snk</AssemblyOriginatorKeyFile>
<PackageId>$(AssemblyName)</PackageId>
<Features>strict</Features>
<Authors>Stack Exchange, Inc.; marc.gravell</Authors>
<Authors>Stack Exchange, Inc.; Marc Gravell; Nick Craver</Authors>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CodeAnalysisRuleset>$(MSBuildThisFileDirectory)Shared.ruleset</CodeAnalysisRuleset>
<MSBuildWarningsAsMessages>NETSDK1069</MSBuildWarningsAsMessages>
Expand Down
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- Adds: Support for `ZMPOP` with `.SortedSetPop()`/`.SortedSetPopAsync()` ([#2094 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2094))
- Adds: Support for `XAUTOCLAIM` with `.StreamAutoClaim()`/.`StreamAutoClaimAsync()` and `.StreamAutoClaimIdsOnly()`/.`StreamAutoClaimIdsOnlyAsync()` ([#2095 by ttingen](https://github.com/StackExchange/StackExchange.Redis/pull/2095))
- Adds: Support for `OBJECT FREQ` with `.KeyFrequency()`/`.KeyFrequencyAsync()` ([#2105 by Avital-Fine](https://github.com/StackExchange/StackExchange.Redis/pull/2105))
- Performance: Avoids allocations when computing cluster hash slots or testing key equality ([#2110 by Marc Gravell](https://github.com/StackExchange/StackExchange.Redis/pull/2110))
- Adds: Support for `SORT_RO` with `.Sort()`/`.SortAsync()` ([#2111 by slorello89](https://github.com/StackExchange/StackExchange.Redis/pull/2111))

## 2.5.61
Expand Down
9 changes: 5 additions & 4 deletions src/StackExchange.Redis/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,11 @@ internal static int VectorSafeIndexOf(this ReadOnlySpan<byte> span, byte value)

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int VectorSafeIndexOfCRLF(this ReadOnlySpan<byte> span)
{
ReadOnlySpan<byte> CRLF = stackalloc byte[2] { (byte)'\r', (byte)'\n' };
return span.IndexOf(CRLF);
}
=> span.IndexOf(CRLF);

// note that this is *not* actually an array; this is compiled into a .data section
// (confirmed down to net472, which is the lowest TFM that uses this branch)
private static ReadOnlySpan<byte> CRLF => new byte[] { (byte)'\r', (byte)'\n' };
#else
internal static int VectorSafeIndexOf(this ReadOnlySpan<byte> span, byte value)
{
Expand Down
232 changes: 182 additions & 50 deletions src/StackExchange.Redis/RedisKey.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace StackExchange.Redis
Expand Down Expand Up @@ -44,115 +47,171 @@ internal bool IsEmpty
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator !=(RedisKey x, RedisKey y) => !(x == y);
public static bool operator !=(RedisKey x, RedisKey y) => !x.EqualsImpl(in y);

/// <summary>
/// Indicate whether two keys are not equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator !=(string x, RedisKey y) => !(x == y);
public static bool operator !=(string x, RedisKey y) => !y.EqualsImpl(new RedisKey(x));

/// <summary>
/// Indicate whether two keys are not equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator !=(byte[] x, RedisKey y) => !(x == y);
public static bool operator !=(byte[] x, RedisKey y) => !y.EqualsImpl(new RedisKey(null, x));

/// <summary>
/// Indicate whether two keys are not equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator !=(RedisKey x, string y) => !(x == y);
public static bool operator !=(RedisKey x, string y) => !x.EqualsImpl(new RedisKey(y));

/// <summary>
/// Indicate whether two keys are not equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator !=(RedisKey x, byte[] y) => !(x == y);
public static bool operator !=(RedisKey x, byte[] y) => !x.EqualsImpl(new RedisKey(null, y));

/// <summary>
/// Indicate whether two keys are equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator ==(RedisKey x, RedisKey y) => CompositeEquals(x.KeyPrefix, x.KeyValue, y.KeyPrefix, y.KeyValue);
public static bool operator ==(RedisKey x, RedisKey y) => x.EqualsImpl(in y);

/// <summary>
/// Indicate whether two keys are equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator ==(string x, RedisKey y) => CompositeEquals(null, x, y.KeyPrefix, y.KeyValue);
public static bool operator ==(string x, RedisKey y) => y.EqualsImpl(new RedisKey(x));

/// <summary>
/// Indicate whether two keys are equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator ==(byte[] x, RedisKey y) => CompositeEquals(null, x, y.KeyPrefix, y.KeyValue);
public static bool operator ==(byte[] x, RedisKey y) => y.EqualsImpl(new RedisKey(null, x));

/// <summary>
/// Indicate whether two keys are equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator ==(RedisKey x, string y) => CompositeEquals(x.KeyPrefix, x.KeyValue, null, y);
public static bool operator ==(RedisKey x, string y) => x.EqualsImpl(new RedisKey(y));

/// <summary>
/// Indicate whether two keys are equal.
/// </summary>
/// <param name="x">The first <see cref="RedisChannel"/> to compare.</param>
/// <param name="y">The second <see cref="RedisChannel"/> to compare.</param>
public static bool operator ==(RedisKey x, byte[] y) => CompositeEquals(x.KeyPrefix, x.KeyValue, null, y);
public static bool operator ==(RedisKey x, byte[] y) => x.EqualsImpl(new RedisKey(null, y));

/// <summary>
/// See <see cref="object.Equals(object?)"/>.
/// </summary>
/// <param name="obj">The <see cref="RedisKey"/> to compare to.</param>
public override bool Equals(object? obj)
public override bool Equals(object? obj) => obj switch
{
if (obj is RedisKey other)
{
return CompositeEquals(KeyPrefix, KeyValue, other.KeyPrefix, other.KeyValue);
}
if (obj is string || obj is byte[])
{
return CompositeEquals(KeyPrefix, KeyValue, null, obj);
}
return false;
}
null => IsNull,
RedisKey key => EqualsImpl(in key),
string s => EqualsImpl(new RedisKey(s)),
byte[] b => EqualsImpl(new RedisKey(null, b)),
_ => false,
};

/// <summary>
/// Indicate whether two keys are equal.
/// </summary>
/// <param name="other">The <see cref="RedisKey"/> to compare to.</param>
public bool Equals(RedisKey other) => CompositeEquals(KeyPrefix, KeyValue, other.KeyPrefix, other.KeyValue);
public bool Equals(RedisKey other) => EqualsImpl(in other);

private static bool CompositeEquals(byte[]? keyPrefix0, object? keyValue0, byte[]? keyPrefix1, object? keyValue1)
private bool EqualsImpl(in RedisKey other)
{
if (RedisValue.Equals(keyPrefix0, keyPrefix1))
if (IsNull)
{
return other.IsNull;
}
else if (other.IsNull)
{
if (keyValue0 == keyValue1) return true; // ref equal
if (keyValue0 == null || keyValue1 == null) return false; // null vs non-null
return false;
}

// if there's no prefix, we might be able to do a simple compare
if (RedisValue.Equals(KeyPrefix, other.KeyPrefix))
{
if ((object?)KeyValue == (object?)other.KeyValue) return true; // ref equal

if (keyValue0 is string keyString1 && keyValue1 is string keyString2) return keyString1 == keyString2;
if (keyValue0 is byte[] keyBytes1 && keyValue1 is byte[] keyBytes2) return RedisValue.Equals(keyBytes1, keyBytes2);
if (KeyValue is string keyString1 && other.KeyValue is string keyString2) return keyString1 == keyString2;
if (KeyValue is byte[] keyBytes1 && other.KeyValue is byte[] keyBytes2) return RedisValue.Equals(keyBytes1, keyBytes2);
}

int len = TotalLength();
if (len != other.TotalLength())
{
return false; // different length; can't be equal
}
if (len == 0)
{
return true; // both empty
}
if (len <= 128)
{
return CopyCompare(in this, in other, len, stackalloc byte[len * 2]);
}
else
{
byte[] arr = ArrayPool<byte>.Shared.Rent(len * 2);
var result = CopyCompare(in this, in other, len, arr);
ArrayPool<byte>.Shared.Return(arr);
return result;
}

return RedisValue.Equals(ConcatenateBytes(keyPrefix0, keyValue0, null), ConcatenateBytes(keyPrefix1, keyValue1, null));
static bool CopyCompare(in RedisKey x, in RedisKey y, int length, Span<byte> span)
{
Span<byte> span1 = span.Slice(0, length), span2 = span.Slice(length, length);
var written = x.CopyTo(span1);
Debug.Assert(written == length, "length error (1)");
written = y.CopyTo(span2);
Debug.Assert(written == length, "length error (2)");
return span1.SequenceEqual(span2);
}
}

/// <inheritdoc/>
public override int GetHashCode()
{
int chk0 = KeyPrefix == null ? 0 : RedisValue.GetHashCode(KeyPrefix),
chk1 = KeyValue is string ? KeyValue.GetHashCode() : RedisValue.GetHashCode((byte[]?)KeyValue);
// note that we need need eaulity-like behavior, regardless of whether the
// parts look like bytes or strings, and with/without prefix

// the simplest way to do this is to use the CopyTo version, which normalizes that
if (IsNull) return -1;
if (TryGetSimpleBuffer(out var buffer)) return RedisValue.GetHashCode(buffer);
var len = TotalLength();
if (len == 0) return 0;

return unchecked((17 * chk0) + chk1);
if (len <= 256)
{
Span<byte> span = stackalloc byte[len];
var written = CopyTo(span);
Debug.Assert(written == len);
return RedisValue.GetHashCode(span);
}
else
{
var arr = ArrayPool<byte>.Shared.Rent(len);
var span = new Span<byte>(arr, 0, len);
var written = CopyTo(span);
Debug.Assert(written == len);
var result = RedisValue.GetHashCode(span);
ArrayPool<byte>.Shared.Return(arr);
return result;
}
}

/// <summary>
Expand Down Expand Up @@ -194,35 +253,55 @@ public static implicit operator RedisKey(byte[]? key)
/// Obtain the <see cref="RedisKey"/> as a <see cref="T:byte[]"/>.
/// </summary>
/// <param name="key">The key to get a byte array for.</param>
public static implicit operator byte[]? (RedisKey key) => ConcatenateBytes(key.KeyPrefix, key.KeyValue, null);
public static implicit operator byte[]? (RedisKey key)
{
if (key.IsNull) return null;
if (key.TryGetSimpleBuffer(out var arr)) return arr;

var len = key.TotalLength();
if (len == 0) return Array.Empty<byte>();
arr = new byte[len];
var written = key.CopyTo(arr);
Debug.Assert(written == len, "length/copyto error");
return arr;
}

/// <summary>
/// Obtain the key as a <see cref="string"/>.
/// </summary>
/// <param name="key">The key to get a string for.</param>
public static implicit operator string? (RedisKey key)
{
byte[]? arr;
if (key.KeyPrefix == null)
if (key.KeyPrefix is null)
{
if (key.KeyValue == null) return null;
return key.KeyValue switch
{
null => null,
string s => s,
object o => Get((byte[])o, -1),
};
}

if (key.KeyValue is string keyString) return keyString;
var len = key.TotalLength();
var arr = ArrayPool<byte>.Shared.Rent(len);
var written = key.CopyTo(arr);
Debug.Assert(written == len, "length error");
var result = Get(arr, len);
ArrayPool<byte>.Shared.Return(arr);
return result;

arr = (byte[])key.KeyValue;
}
else
static string? Get(byte[] arr, int length)
{
arr = (byte[]?)key;
}
if (arr == null) return null;
try
{
return Encoding.UTF8.GetString(arr);
}
catch
{
return BitConverter.ToString(arr);
if (length == -1) length = arr.Length;
if (length == 0) return "";
try
{
return Encoding.UTF8.GetString(arr, 0, length);
}
catch
{
return BitConverter.ToString(arr, 0, length);
}
}
}

Expand Down Expand Up @@ -297,5 +376,58 @@ 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 NETCOREAPP
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;
}
}
}
Loading