Skip to content

Commit 512f33a

Browse files
Merge pull request #984 from stebet/decimalSerialization
Speeding up decimal (de)serialization. (cherry picked from commit 830c747)
1 parent 1dd70ce commit 512f33a

File tree

2 files changed

+126
-45
lines changed

2 files changed

+126
-45
lines changed

projects/RabbitMQ.Client/client/impl/WireFormatting.cs

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,45 @@ namespace RabbitMQ.Client.Impl
4141
{
4242
internal class WireFormatting
4343
{
44-
public static decimal AmqpToDecimal(byte scale, uint unsignedMantissa)
44+
// * DESCRIPTION TAKEN FROM MS REFERENCE SOURCE *
45+
// https://github.com/microsoft/referencesource/blob/master/mscorlib/system/decimal.cs
46+
// The lo, mid, hi, and flags fields contain the representation of the
47+
// Decimal value. The lo, mid, and hi fields contain the 96-bit integer
48+
// part of the Decimal. Bits 0-15 (the lower word) of the flags field are
49+
// unused and must be zero; bits 16-23 contain must contain a value between
50+
// 0 and 28, indicating the power of 10 to divide the 96-bit integer part
51+
// by to produce the Decimal value; bits 24-30 are unused and must be zero;
52+
// and finally bit 31 indicates the sign of the Decimal value, 0 meaning
53+
// positive and 1 meaning negative.
54+
readonly struct DecimalData
55+
{
56+
public readonly uint Flags;
57+
public readonly uint Hi;
58+
public readonly uint Lo;
59+
public readonly uint Mid;
60+
61+
internal DecimalData(uint flags, uint hi, uint lo, uint mid)
62+
{
63+
Flags = flags;
64+
Hi = hi;
65+
Lo = lo;
66+
Mid = mid;
67+
}
68+
}
69+
70+
private static UTF8Encoding UTF8 = new UTF8Encoding();
71+
72+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
73+
public static decimal ReadDecimal(ReadOnlySpan<byte> span)
4574
{
4675
if (scale > 28)
4776
{
48-
throw new SyntaxErrorException($"Unrepresentable AMQP decimal table field: scale={scale}");
77+
ThrowInvalidDecimalScale(scale);
4978
}
5079

51-
return new decimal(
52-
// The low 32 bits of a 96-bit integer
53-
lo: (int)(unsignedMantissa & 0x7FFFFFFF),
54-
// The middle 32 bits of a 96-bit integer.
55-
mid: 0,
56-
// The high 32 bits of a 96-bit integer.
57-
hi: 0,
58-
isNegative: (unsignedMantissa & 0x80000000) != 0,
59-
// A power of 10 ranging from 0 to 28.
60-
scale: scale);
80+
uint unsignedMantissa = NetworkOrderDeserializer.ReadUInt32(span.Slice(1));
81+
var data = new DecimalData(((uint)(scale << 16)) | unsignedMantissa & 0x80000000, 0, unsignedMantissa & 0x7FFFFFFF, 0);
82+
return Unsafe.As<DecimalData, decimal>(ref data);
6183
}
6284

6385
public static void DecimalToAmqp(decimal value, out byte scale, out int mantissa)
@@ -274,11 +296,37 @@ public static int GetArrayByteCount(IList val)
274296
return byteCount;
275297
}
276298

299+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
300+
#if NETCOREAPP
301+
public static int GetByteCount(ReadOnlySpan<char> val) => val.IsEmpty ? 0 : UTF8.GetByteCount(val);
302+
#else
303+
public static int GetByteCount(string val) => string.IsNullOrEmpty(val) ? 0 : UTF8.GetByteCount(val);
304+
#endif
305+
306+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
277307
public static int WriteDecimal(Span<byte> span, decimal value)
278308
{
279-
DecimalToAmqp(value, out byte scale, out int mantissa);
280-
span[0] = scale;
281-
return 1 + WriteLong(span.Slice(1), (uint)mantissa);
309+
// Cast the decimal to our struct to avoid the decimal.GetBits allocations.
310+
DecimalData data = Unsafe.As<decimal, DecimalData>(ref value);
311+
// According to the documentation :-
312+
// - word 0: low-order "mantissa"
313+
// - word 1, word 2: medium- and high-order "mantissa"
314+
// - word 3: mostly reserved; "exponent" and sign bit
315+
// In one way, this is broader than AMQP: the mantissa is larger.
316+
// In another way, smaller: the exponent ranges 0-28 inclusive.
317+
// We need to be careful about the range of word 0, too: we can
318+
// only take 31 bits worth of it, since the sign bit needs to
319+
// fit in there too.
320+
if (data.Mid != 0 || // mantissa extends into middle word
321+
data.Hi != 0 || // mantissa extends into top word
322+
data.Lo < 0) // mantissa extends beyond 31 bits
323+
{
324+
return ThrowWireFormattingException(value);
325+
}
326+
327+
span[0] = (byte)((data.Flags >> 16) & 0xFF);
328+
WriteLong(span.Slice(1), (data.Flags & 0b1000_0000_0000_0000_0000_0000_0000_0000) | (data.Lo & 0b0111_1111_1111_1111_1111_1111_1111_1111));
329+
return 5;
282330
}
283331

284332
public static int WriteFieldValue(Span<byte> span, object value)
@@ -558,5 +606,30 @@ public static int WriteTimestamp(Span<byte> span, AmqpTimestamp val)
558606
// See also MethodArgumentReader.ReadTimestamp and AmqpTimestamp itself
559607
return WriteLonglong(span, (ulong)val.UnixTime);
560608
}
609+
610+
public static int ThrowArgumentOutOfRangeException(int orig, int expected)
611+
{
612+
throw new ArgumentOutOfRangeException("span", $"Span has not enough space ({orig} instead of {expected})");
613+
}
614+
615+
public static int ThrowArgumentOutOfRangeException(string val, int maxLength)
616+
{
617+
throw new ArgumentOutOfRangeException(nameof(val), val, $"Value exceeds the maximum allowed length of {maxLength} bytes.");
618+
}
619+
620+
public static int ThrowSyntaxErrorException(uint byteCount)
621+
{
622+
throw new SyntaxErrorException($"Long string too long; byte length={byteCount}, max={int.MaxValue}");
623+
}
624+
625+
private static int ThrowWireFormattingException(decimal value)
626+
{
627+
throw new WireFormattingException("Decimal overflow in AMQP encoding", value);
628+
}
629+
630+
private static decimal ThrowInvalidDecimalScale(int scale)
631+
{
632+
throw new SyntaxErrorException($"Unrepresentable AMQP decimal table field: scale={scale}");
633+
}
561634
}
562635
}

0 commit comments

Comments
 (0)