Skip to content
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
3 changes: 2 additions & 1 deletion docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ Current package versions:
- Fix key-prefix omission in `SetIntersectionLength` and `SortedSet{Combine[WithScores]|IntersectionLength}` ([#2863 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2863))
- Add `Condition.SortedSet[Not]ContainsStarting` condition for transactions ([#2638 by ArnoKoll](https://github.com/StackExchange/StackExchange.Redis/pull/2638))
- Add support for XPENDING Idle time filter ([#2822 by david-brink-talogy](https://github.com/StackExchange/StackExchange.Redis/pull/2822))
-
- Improve `double` formatting performance on net8+ ([#2928 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2928))

## 2.8.58

- Fix [#2679](https://github.com/StackExchange/StackExchange.Redis/issues/2679) - blocking call in long-running connects ([#2680 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2680))
Expand Down
133 changes: 84 additions & 49 deletions src/StackExchange.Redis/Format.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;

#if UNIX_SOCKET
Expand Down Expand Up @@ -168,24 +169,41 @@ internal static bool TryParseDouble(string? s, out double value)
value = s[0] - '0';
return true;
// RESP3 spec demands inf/nan handling
case 3 when CaseInsensitiveASCIIEqual("inf", s):
value = double.PositiveInfinity;
return true;
case 3 when CaseInsensitiveASCIIEqual("nan", s):
value = double.NaN;
return true;
case 4 when CaseInsensitiveASCIIEqual("+inf", s):
value = double.PositiveInfinity;
return true;
case 4 when CaseInsensitiveASCIIEqual("-inf", s):
value = double.NegativeInfinity;
return true;
case 4 when CaseInsensitiveASCIIEqual("+nan", s):
case 4 when CaseInsensitiveASCIIEqual("-nan", s):
value = double.NaN;
case 3 when TryParseInfNaN(s.AsSpan(), true, out value):
case 4 when s[0] == '+' && TryParseInfNaN(s.AsSpan(1), true, out value):
case 4 when s[0] == '-' && TryParseInfNaN(s.AsSpan(1), false, out value):
return true;
}
return double.TryParse(s, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out value);

static bool TryParseInfNaN(ReadOnlySpan<char> s, bool positive, out double value)
{
switch (s[0])
{
case 'i':
case 'I':
if (s[1] is 'n' or 'N' && s[2] is 'f' or 'F')
{
value = positive ? double.PositiveInfinity : double.NegativeInfinity;
return true;
}
break;
case 'n':
case 'N':
if (s[1] is 'a' or 'A' && s[2] is 'n' or 'N')
{
value = double.NaN;
return true;
}
break;
}
#if NET6_0_OR_GREATER
Unsafe.SkipInit(out value);
#else
value = 0;
#endif
return false;
}
}

internal static bool TryParseUInt64(string s, out ulong value) =>
Expand Down Expand Up @@ -235,37 +253,41 @@ internal static bool TryParseDouble(ReadOnlySpan<byte> s, out double value)
value = s[0] - '0';
return true;
// RESP3 spec demands inf/nan handling
case 3 when CaseInsensitiveASCIIEqual("inf", s):
value = double.PositiveInfinity;
return true;
case 3 when CaseInsensitiveASCIIEqual("nan", s):
value = double.NaN;
return true;
case 4 when CaseInsensitiveASCIIEqual("+inf", s):
value = double.PositiveInfinity;
return true;
case 4 when CaseInsensitiveASCIIEqual("-inf", s):
value = double.NegativeInfinity;
return true;
case 4 when CaseInsensitiveASCIIEqual("+nan", s):
case 4 when CaseInsensitiveASCIIEqual("-nan", s):
value = double.NaN;
case 3 when TryParseInfNaN(s, true, out value):
case 4 when s[0] == '+' && TryParseInfNaN(s.Slice(1), true, out value):
case 4 when s[0] == '-' && TryParseInfNaN(s.Slice(1), false, out value):
return true;
}
return Utf8Parser.TryParse(s, out value, out int bytes) & bytes == s.Length;
}

private static bool CaseInsensitiveASCIIEqual(string xLowerCase, string y)
=> string.Equals(xLowerCase, y, StringComparison.OrdinalIgnoreCase);

private static bool CaseInsensitiveASCIIEqual(string xLowerCase, ReadOnlySpan<byte> y)
{
if (y.Length != xLowerCase.Length) return false;
for (int i = 0; i < y.Length; i++)
static bool TryParseInfNaN(ReadOnlySpan<byte> s, bool positive, out double value)
{
if (char.ToLower((char)y[i]) != xLowerCase[i]) return false;
switch (s[0])
{
case (byte)'i':
case (byte)'I':
if (s[1] is (byte)'n' or (byte)'N' && s[2] is (byte)'f' or (byte)'F')
{
value = positive ? double.PositiveInfinity : double.NegativeInfinity;
return true;
}
break;
case (byte)'n':
case (byte)'N':
if (s[1] is (byte)'a' or (byte)'A' && s[2] is (byte)'n' or (byte)'N')
{
value = double.NaN;
return true;
}
break;
}
#if NET6_0_OR_GREATER
Unsafe.SkipInit(out value);
#else
value = 0;
#endif
return false;
}
return true;
}

/// <summary>
Expand Down Expand Up @@ -399,11 +421,21 @@ internal static unsafe string GetString(ReadOnlySpan<byte> span)

internal const int
MaxInt32TextLen = 11, // -2,147,483,648 (not including the commas)
MaxInt64TextLen = 20; // -9,223,372,036,854,775,808 (not including the commas)
MaxInt64TextLen = 20, // -9,223,372,036,854,775,808 (not including the commas),
MaxDoubleTextLen = 40; // we use G17, allow for sign/E/and allow plenty of panic room

internal static int MeasureDouble(double value)
{
if (double.IsInfinity(value)) return 4; // +inf / -inf

#if NET8_0_OR_GREATER // can use IUtf8Formattable
Span<byte> buffer = stackalloc byte[MaxDoubleTextLen];
if (value.TryFormat(buffer, out int len, "G17", NumberFormatInfo.InvariantInfo))
{
return len;
}
#endif
// fallback (TFM or unexpected size)
var s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct
return s.Length;
}
Expand All @@ -412,16 +444,18 @@ internal static int FormatDouble(double value, Span<byte> destination)
{
if (double.IsInfinity(value))
{
if (double.IsPositiveInfinity(value))
{
if (!"+inf"u8.TryCopyTo(destination)) ThrowFormatFailed();
}
else
{
if (!"-inf"u8.TryCopyTo(destination)) ThrowFormatFailed();
}
if (!(double.IsPositiveInfinity(value) ? "+inf"u8 : "-inf"u8).TryCopyTo(destination)) ThrowFormatFailed();
return 4;
}

#if NET8_0_OR_GREATER // can use IUtf8Formattable
if (!value.TryFormat(destination, out int len, "G17", NumberFormatInfo.InvariantInfo))
{
ThrowFormatFailed();
}

return len;
#else
var s = value.ToString("G17", NumberFormatInfo.InvariantInfo); // this looks inefficient, but is how Utf8Formatter works too, just: more direct
if (s.Length > destination.Length) ThrowFormatFailed();

Expand All @@ -431,6 +465,7 @@ internal static int FormatDouble(double value, Span<byte> destination)
destination[i] = (byte)chars[i];
}
return chars.Length;
#endif
}

internal static int MeasureInt64(long value)
Expand Down
39 changes: 31 additions & 8 deletions src/StackExchange.Redis/PhysicalConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,9 @@ internal static void WriteBulkString(in RedisValue value, PipeWriter? maybeNullW
case RedisValue.StorageType.UInt64:
WriteUnifiedUInt64(writer, value.OverlappedValueUInt64);
break;
case RedisValue.StorageType.Double: // use string
case RedisValue.StorageType.Double:
WriteUnifiedDouble(writer, value.OverlappedValueDouble);
break;
case RedisValue.StorageType.String:
WriteUnifiedPrefixedString(writer, null, (string?)value);
break;
Expand Down Expand Up @@ -1341,9 +1343,9 @@ private static void WriteUnifiedInt64(PipeWriter writer, long value)
// note from specification: A client sends to the Redis server a RESP Array consisting of just Bulk Strings.
// (i.e. we can't just send ":123\r\n", we need to send "$3\r\n123\r\n"

// ${asc-len}\r\n = 3 + MaxInt32TextLen
// ${asc-len}\r\n = 4/5 (asc-len at most 2 digits)
// {asc}\r\n = MaxInt64TextLen + 2
var span = writer.GetSpan(5 + Format.MaxInt32TextLen + Format.MaxInt64TextLen);
var span = writer.GetSpan(7 + Format.MaxInt64TextLen);

span[0] = (byte)'$';
var bytes = WriteRaw(span, value, withLengthPrefix: true, offset: 1);
Expand All @@ -1354,20 +1356,41 @@ private static void WriteUnifiedUInt64(PipeWriter writer, ulong value)
{
// note from specification: A client sends to the Redis server a RESP Array consisting of just Bulk Strings.
// (i.e. we can't just send ":123\r\n", we need to send "$3\r\n123\r\n"

// ${asc-len}\r\n = 3 + MaxInt32TextLen
// {asc}\r\n = MaxInt64TextLen + 2
var span = writer.GetSpan(5 + Format.MaxInt32TextLen + Format.MaxInt64TextLen);

Span<byte> valueSpan = stackalloc byte[Format.MaxInt64TextLen];

var len = Format.FormatUInt64(value, valueSpan);
// ${asc-len}\r\n = 4/5 (asc-len at most 2 digits)
// {asc}\r\n = {len} + 2
var span = writer.GetSpan(7 + len);
span[0] = (byte)'$';
int offset = WriteRaw(span, len, withLengthPrefix: false, offset: 1);
valueSpan.Slice(0, len).CopyTo(span.Slice(offset));
offset += len;
offset = WriteCrlf(span, offset);
writer.Advance(offset);
}

private static void WriteUnifiedDouble(PipeWriter writer, double value)
{
#if NET8_0_OR_GREATER
Span<byte> valueSpan = stackalloc byte[Format.MaxDoubleTextLen];
var len = Format.FormatDouble(value, valueSpan);

// ${asc-len}\r\n = 4/5 (asc-len at most 2 digits)
// {asc}\r\n = {len} + 2
var span = writer.GetSpan(7 + len);
span[0] = (byte)'$';
int offset = WriteRaw(span, len, withLengthPrefix: false, offset: 1);
valueSpan.Slice(0, len).CopyTo(span.Slice(offset));
offset += len;
offset = WriteCrlf(span, offset);
writer.Advance(offset);
#else
// fallback: drop to string
WriteUnifiedPrefixedString(writer, null, Format.ToString(value));
#endif
}

internal static void WriteInteger(PipeWriter writer, long value)
{
// note: client should never write integer; only server does this
Expand Down
12 changes: 8 additions & 4 deletions src/StackExchange.Redis/RedisValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public bool IsNullOrEmpty
/// <param name="y">The second <see cref="RedisValue"/> to compare.</param>
public static bool operator !=(RedisValue x, RedisValue y) => !(x == y);

private double OverlappedValueDouble
internal double OverlappedValueDouble
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => BitConverter.Int64BitsToDouble(_overlappedBits64);
Expand Down Expand Up @@ -849,7 +849,7 @@ private static string ToHex(ReadOnlySpan<byte> src)
len = Format.FormatUInt64(value.OverlappedValueUInt64, span);
return span.Slice(0, len).ToArray();
case StorageType.Double:
span = stackalloc byte[128];
span = stackalloc byte[Format.MaxDoubleTextLen];
len = Format.FormatDouble(value.OverlappedValueDouble, span);
return span.Slice(0, len).ToArray();
case StorageType.String:
Expand Down Expand Up @@ -986,7 +986,8 @@ internal RedisValue Simplify()
if (Format.TryParseInt64(s, out i64)) return i64;
if (Format.TryParseUInt64(s, out u64)) return u64;
}
if (Format.TryParseDouble(s, out var f64)) return f64;
// note: don't simplify inf/nan, as that causes equality semantic problems
if (Format.TryParseDouble(s, out var f64) && !IsSpecialDouble(f64)) return f64;
break;
case StorageType.Raw:
var b = _memory.Span;
Expand All @@ -995,7 +996,8 @@ internal RedisValue Simplify()
if (Format.TryParseInt64(b, out i64)) return i64;
if (Format.TryParseUInt64(b, out u64)) return u64;
}
if (TryParseDouble(b, out f64)) return f64;
// note: don't simplify inf/nan, as that causes equality semantic problems
if (TryParseDouble(b, out f64) && !IsSpecialDouble(f64)) return f64;
break;
case StorageType.Double:
// is the double actually an integer?
Expand All @@ -1006,6 +1008,8 @@ internal RedisValue Simplify()
return this;
}

private static bool IsSpecialDouble(double d) => double.IsNaN(d) || double.IsInfinity(d);

/// <summary>
/// Convert to a signed <see cref="long"/> if possible.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions tests/StackExchange.Redis.Tests/KeyAndValueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ public void TestValues()

internal static void CheckSame(RedisValue x, RedisValue y)
{
if (x.TryParse(out double value) && double.IsNaN(value))
{
// NaN has atypical equality rules
Assert.True(y.TryParse(out value) && double.IsNaN(value));
return;
}
Assert.True(Equals(x, y), "Equals(x, y)");
Assert.True(Equals(y, x), "Equals(y, x)");
Assert.True(EqualityComparer<RedisValue>.Default.Equals(x, y), "EQ(x,y)");
Expand Down
Loading
Loading