Skip to content

Speeding up decimal (de)serialization. #984

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

Merged
merged 1 commit into from
Dec 14, 2020
Merged
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
77 changes: 51 additions & 26 deletions projects/RabbitMQ.Client/client/impl/WireFormatting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,27 +42,46 @@ namespace RabbitMQ.Client.Impl
{
internal static class WireFormatting
{
// * DESCRIPTION TAKEN FROM MS REFERENCE SOURCE *
// https://github.com/microsoft/referencesource/blob/master/mscorlib/system/decimal.cs
// The lo, mid, hi, and flags fields contain the representation of the
// Decimal value. The lo, mid, and hi fields contain the 96-bit integer
// part of the Decimal. Bits 0-15 (the lower word) of the flags field are
// unused and must be zero; bits 16-23 contain must contain a value between
// 0 and 28, indicating the power of 10 to divide the 96-bit integer part
// by to produce the Decimal value; bits 24-30 are unused and must be zero;
// and finally bit 31 indicates the sign of the Decimal value, 0 meaning
// positive and 1 meaning negative.
readonly struct DecimalData
{
public readonly uint Flags;
public readonly uint Hi;
public readonly uint Lo;
public readonly uint Mid;

internal DecimalData(uint flags, uint hi, uint lo, uint mid)
{
Flags = flags;
Hi = hi;
Lo = lo;
Mid = mid;
}
}

private static UTF8Encoding UTF8 = new UTF8Encoding();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static decimal ReadDecimal(ReadOnlySpan<byte> span)
{
byte scale = span[0];
if (scale > 28)
{
throw new SyntaxErrorException($"Unrepresentable AMQP decimal table field: scale={scale}");
ThrowInvalidDecimalScale(scale);
}

uint unsignedMantissa = NetworkOrderDeserializer.ReadUInt32(span.Slice(1));
return new decimal(
// The low 32 bits of a 96-bit integer
lo: (int)(unsignedMantissa & 0x7FFFFFFF),
// The middle 32 bits of a 96-bit integer.
mid: 0,
// The high 32 bits of a 96-bit integer.
hi: 0,
isNegative: (unsignedMantissa & 0x80000000) != 0,
// A power of 10 ranging from 0 to 28.
scale: scale);
var data = new DecimalData(((uint)(scale << 16)) | unsignedMantissa & 0x80000000, 0, unsignedMantissa & 0x7FFFFFFF, 0);
return Unsafe.As<DecimalData, decimal>(ref data);
}

public static IList ReadArray(ReadOnlySpan<byte> span, out int bytesRead)
Expand Down Expand Up @@ -360,15 +379,11 @@ public static int GetArrayByteCount(IList val)
public static int GetByteCount(string val) => string.IsNullOrEmpty(val) ? 0 : UTF8.GetByteCount(val);
#endif

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int WriteDecimal(Span<byte> span, decimal value)
{
DecimalToAmqp(value, out byte scale, out int mantissa);
span[0] = scale;
return 1 + WriteLong(span.Slice(1), (uint)mantissa);
}

private static void DecimalToAmqp(decimal value, out byte scale, out int mantissa)
{
// Cast the decimal to our struct to avoid the decimal.GetBits allocations.
DecimalData data = Unsafe.As<decimal, DecimalData>(ref value);
// According to the documentation :-
// - word 0: low-order "mantissa"
// - word 1, word 2: medium- and high-order "mantissa"
Expand All @@ -378,16 +393,16 @@ private static void DecimalToAmqp(decimal value, out byte scale, out int mantiss
// We need to be careful about the range of word 0, too: we can
// only take 31 bits worth of it, since the sign bit needs to
// fit in there too.
int[] bitRepresentation = decimal.GetBits(value);
if (bitRepresentation[1] != 0 || // mantissa extends into middle word
bitRepresentation[2] != 0 || // mantissa extends into top word
bitRepresentation[0] < 0) // mantissa extends beyond 31 bits
if (data.Mid != 0 || // mantissa extends into middle word
data.Hi != 0 || // mantissa extends into top word
data.Lo < 0) // mantissa extends beyond 31 bits
{
throw new WireFormattingException("Decimal overflow in AMQP encoding", value);
return ThrowWireFormattingException(value);
}
scale = (byte)((((uint)bitRepresentation[3]) >> 16) & 0xFF);
mantissa = (int)((((uint)bitRepresentation[3]) & 0x80000000) |
(((uint)bitRepresentation[0]) & 0x7FFFFFFF));

span[0] = (byte)((data.Flags >> 16) & 0xFF);
WriteLong(span.Slice(1), (data.Flags & 0b1000_0000_0000_0000_0000_0000_0000_0000) | (data.Lo & 0b0111_1111_1111_1111_1111_1111_1111_1111));
return 5;
}

public static int WriteFieldValue(Span<byte> span, object value)
Expand Down Expand Up @@ -908,5 +923,15 @@ public static int ThrowSyntaxErrorException(uint byteCount)
{
throw new SyntaxErrorException($"Long string too long; byte length={byteCount}, max={int.MaxValue}");
}

private static int ThrowWireFormattingException(decimal value)
{
throw new WireFormattingException("Decimal overflow in AMQP encoding", value);
}

private static decimal ThrowInvalidDecimalScale(int scale)
{
throw new SyntaxErrorException($"Unrepresentable AMQP decimal table field: scale={scale}");
}
}
}