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

BigInteger parsing optimizations #47842

Merged
merged 4 commits into from
May 9, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ public BigInteger(ReadOnlySpan<byte> value, bool isUnsigned = false, bool isBigE
AssertValid();
}

private BigInteger(int n, uint[]? rgu)
internal BigInteger(int n, uint[]? rgu)
{
_sign = n;
_bits = rgu;
Expand Down Expand Up @@ -703,7 +703,7 @@ public static BigInteger Parse(ReadOnlySpan<char> value, NumberStyles style = Nu

public static bool TryParse(ReadOnlySpan<char> value, out BigInteger result)
{
return BigNumber.TryParseBigInteger(value, NumberStyles.Integer, NumberFormatInfo.CurrentInfo, out result);
return TryParse(value, NumberStyles.Integer, NumberFormatInfo.CurrentInfo, out result);
}

public static bool TryParse(ReadOnlySpan<char> value, NumberStyles style, IFormatProvider? provider, out BigInteger result)
Expand Down
301 changes: 239 additions & 62 deletions src/libraries/System.Runtime.Numerics/src/System/Numerics/BigNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;

namespace System.Numerics
Expand All @@ -286,6 +287,8 @@ internal static class BigNumber
| NumberStyles.AllowThousands | NumberStyles.AllowExponent
| NumberStyles.AllowCurrencySymbol | NumberStyles.AllowHexSpecifier);

private static readonly uint[] s_uint32PowersOfTen = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 };
tannergooding marked this conversation as resolved.
Show resolved Hide resolved

private struct BigNumberBuffer
{
public StringBuilder digits;
Expand Down Expand Up @@ -326,7 +329,7 @@ internal static bool TryParseBigInteger(string? value, NumberStyles style, Numbe
{
if (value == null)
{
result = default(BigInteger);
result = default;
return false;
}

Expand All @@ -335,33 +338,32 @@ internal static bool TryParseBigInteger(string? value, NumberStyles style, Numbe

internal static bool TryParseBigInteger(ReadOnlySpan<char> value, NumberStyles style, NumberFormatInfo info, out BigInteger result)
{
unsafe
{
result = BigInteger.Zero;
ArgumentException? e;
if (!TryValidateParseStyleInteger(style, out e))
throw e; // TryParse still throws ArgumentException on invalid NumberStyles
ArgumentException? e;
if (!TryValidateParseStyleInteger(style, out e))
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
throw e; // TryParse still throws ArgumentException on invalid NumberStyles

BigNumberBuffer bignumber = BigNumberBuffer.Create();
if (!FormatProvider.TryStringToBigInteger(value, style, info, bignumber.digits, out bignumber.precision, out bignumber.scale, out bignumber.sign))
return false;
BigNumberBuffer bignumber = BigNumberBuffer.Create();
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
if (!FormatProvider.TryStringToBigInteger(value, style, info, bignumber.digits, out bignumber.precision, out bignumber.scale, out bignumber.sign))
{
result = default;
return false;
}

if ((style & NumberStyles.AllowHexSpecifier) != 0)
if ((style & NumberStyles.AllowHexSpecifier) != 0)
{
if (!HexNumberToBigInteger(ref bignumber, out result))
{
if (!HexNumberToBigInteger(ref bignumber, ref result))
{
return false;
}
return false;
}
else
}
else
{
if (!NumberToBigInteger(ref bignumber, out result))
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
{
if (!NumberToBigInteger(ref bignumber, ref result))
{
return false;
}
return false;
}
return true;
}
return true;
}

internal static BigInteger ParseBigInteger(string value, NumberStyles style, NumberFormatInfo info)
Expand All @@ -380,79 +382,254 @@ internal static BigInteger ParseBigInteger(ReadOnlySpan<char> value, NumberStyle
if (!TryValidateParseStyleInteger(style, out e))
throw e;

BigInteger result = BigInteger.Zero;
if (!TryParseBigInteger(value, style, info, out result))
if (!TryParseBigInteger(value, style, info, out BigInteger result))
{
throw new FormatException(SR.Overflow_ParseBigInteger);
}
return result;
}

private static unsafe bool HexNumberToBigInteger(ref BigNumberBuffer number, ref BigInteger value)
private static bool HexNumberToBigInteger(ref BigNumberBuffer number, out BigInteger result)
{
if (number.digits == null || number.digits.Length == 0)
{
result = default;
return false;
}

const int DigitsPerBlock = 8;

int totalDigitCount = number.digits.Length - 1; // Ignore trailing '\0'
int blockCount, partialDigitCount;

int len = number.digits.Length - 1; // Ignore trailing '\0'
byte[] bits = new byte[(len / 2) + (len % 2)];
if ((uint)totalDigitCount % DigitsPerBlock == 0)
{
blockCount = totalDigitCount / DigitsPerBlock;
partialDigitCount = 0;
}
else
{
blockCount = Math.DivRem(totalDigitCount, DigitsPerBlock, out int mod) + 1;
partialDigitCount = DigitsPerBlock - mod;
}
tannergooding marked this conversation as resolved.
Show resolved Hide resolved

bool isNegative = HexConverter.FromChar(number.digits[0]) >= 8;
uint partialValue = (isNegative && partialDigitCount > 0) ? 0xFFFFFFFFu : 0;

int[]? arrayFromPool = null;

bool shift = false;
bool isNegative = false;
int bitIndex = 0;
Span<int> bitsBuffer = (blockCount <= BigInteger.StackallocUInt32Limit)
? stackalloc int[blockCount]
: (arrayFromPool = ArrayPool<int>.Shared.Rent(blockCount)).AsSpan(0, blockCount);
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
tannergooding marked this conversation as resolved.
Show resolved Hide resolved

// Parse the string into a little-endian two's complement byte array
// string value : O F E B 7 \0
// string index (i) : 0 1 2 3 4 5 <--
// byte[] (bitIndex): 2 1 1 0 0 <--
//
for (int i = len - 1; i > -1; i--)
int bitsBufferPos = blockCount - 1;

try
{
char c = number.digits[i];
Copy link
Member

Choose a reason for hiding this comment

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

@stephentoub, is this an opportunity for an analyzer? Indexing into StringBuilder can be deceptively slow due to its internal chunking.

int b = HexConverter.FromChar(c);
Debug.Assert(b != 0xFF);
if (i == 0 && (b & 0x08) == 0x08)
isNegative = true;
foreach (ReadOnlyMemory<char> digitsChunkMem in number.digits.GetChunks())
{
ReadOnlySpan<char> chunkDigits = digitsChunkMem.Span;
Copy link
Member

Choose a reason for hiding this comment

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

Likewise, I wonder if we should have a (internal for now) CurrentSpan property that is slightly more efficient.

We don't really need to return a ReadOnlyMemory when we only want to extract the underlying Span<T>, particularly when the ROM<T> is just being constructed over a backing array.

It's, in practice, no more unsafe than say CollectionsMarshal.AsSpan

for (int i = 0; i < chunkDigits.Length; i++)
{
char digitChar = chunkDigits[i];
if (digitChar == '\0')
break;
tannergooding marked this conversation as resolved.
Show resolved Hide resolved

int hexValue = HexConverter.FromChar(digitChar);
Debug.Assert(hexValue != 0xFF);

partialValue = (partialValue << 4) | (uint)hexValue;
partialDigitCount++;

if (partialDigitCount == DigitsPerBlock)
{
bitsBuffer[bitsBufferPos] = (int)partialValue;
bitsBufferPos--;
partialValue = 0;
partialDigitCount = 0;
}
}
}

Debug.Assert(partialDigitCount == 0 && bitsBufferPos == -1);

// BigInteger requires leading zero blocks to be truncated.
bitsBuffer = bitsBuffer.TrimEnd(0);

int sign;
uint[]? bits;

if (shift)
if (bitsBuffer.IsEmpty)
{
bits[bitIndex] = (byte)(bits[bitIndex] | (b << 4));
bitIndex++;
sign = 0;
bits = null;
}
else if (bitsBuffer.Length == 1)
{
sign = bitsBuffer[0];
bits = null;

if ((!isNegative && sign < 0) || sign == int.MinValue)
{
sign = isNegative ? -1 : 1;
bits = new[] { (uint)sign };
}
}
else
{
bits[bitIndex] = (byte)(isNegative ? (b | 0xF0) : (b));
sign = isNegative ? -1 : 1;
bits = MemoryMarshal.Cast<int, uint>(bitsBuffer).ToArray();

if (isNegative)
NumericsHelpers.DangerousMakeTwosComplement(bits);
}
shift = !shift;
}

value = new BigInteger(bits);
return true;
result = new BigInteger(sign, bits);
return true;
}
finally
{
if (arrayFromPool != null)
ArrayPool<int>.Shared.Return(arrayFromPool);
}
}

private static unsafe bool NumberToBigInteger(ref BigNumberBuffer number, ref BigInteger value)
private static bool NumberToBigInteger(ref BigNumberBuffer number, out BigInteger result)
{
int i = number.scale;
int cur = 0;
Span<int> stackBuffer = stackalloc int[BigInteger.StackallocUInt32Limit];
Span<int> currentBuffer = stackBuffer;
int currentBufferSize = 0;
int[]? arrayFromPool = null;

uint partialValue = 0;
int partialDigitCount = 0;
int totalDigitCount = 0;
int numberScale = number.scale;

const int MaxPartialDigits = 9;
const uint TenPowMaxPartial = 1000000000;

BigInteger ten = 10;
value = 0;
while (--i >= 0)
try
{
value *= ten;
if (number.digits[cur] != '\0')
foreach (ReadOnlyMemory<char> digitsChunk in number.digits.GetChunks())
{
if (!ProcessChunk(digitsChunk.Span, ref currentBuffer))
{
result = default;
return false;
}
}

if (partialDigitCount > 0)
MultiplyAdd(ref currentBuffer, s_uint32PowersOfTen[partialDigitCount], partialValue);

int trailingZeroCount = numberScale - totalDigitCount;

while (trailingZeroCount >= MaxPartialDigits)
{
MultiplyAdd(ref currentBuffer, TenPowMaxPartial, 0);
trailingZeroCount -= MaxPartialDigits;
}

if (trailingZeroCount > 0)
MultiplyAdd(ref currentBuffer, s_uint32PowersOfTen[trailingZeroCount], 0);

int sign;
uint[]? bits;

if (currentBufferSize == 0)
{
value += number.digits[cur++] - '0';
sign = 0;
bits = null;
}
else if (currentBufferSize == 1 && (uint)currentBuffer[0] <= int.MaxValue)
{
sign = number.sign ? -currentBuffer[0] : currentBuffer[0];
bits = null;
}
else
{
sign = number.sign ? -1 : 1;
bits = MemoryMarshal.Cast<int, uint>(currentBuffer).Slice(0, currentBufferSize).ToArray();
}

result = new BigInteger(sign, bits);
return true;
}
while (number.digits[cur] != '\0')
finally
{
if (number.digits[cur++] != '0') return false; // Disallow non-zero trailing decimal places
if (arrayFromPool != null)
ArrayPool<int>.Shared.Return(arrayFromPool);
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
}
if (number.sign)

bool ProcessChunk(ReadOnlySpan<char> chunkDigits, ref Span<int> currentBuffer)
{
int remainingIntDigitCount = Math.Max(numberScale - totalDigitCount, 0);
ReadOnlySpan<char> intDigitsSpan = chunkDigits.Slice(0, Math.Min(remainingIntDigitCount, chunkDigits.Length));

for (int i = 0; i < intDigitsSpan.Length; i++)
{
char digitChar = chunkDigits[i];
if (digitChar == '\0')
return true;

partialValue = partialValue * 10 + (uint)(digitChar - '0');
jeffhandley marked this conversation as resolved.
Show resolved Hide resolved
partialDigitCount++;
totalDigitCount++;

// Update the buffer when enough partial digits have been accumulated.
if (partialDigitCount == MaxPartialDigits)
{
MultiplyAdd(ref currentBuffer, TenPowMaxPartial, partialValue);
partialValue = 0;
partialDigitCount = 0;
}
}

// Check for nonzero digits after the decimal point.
ReadOnlySpan<char> fracDigitsSpan = chunkDigits.Slice(intDigitsSpan.Length);
for (int i = 0; i < fracDigitsSpan.Length; i++)
{
char digitChar = fracDigitsSpan[i];
if (digitChar == '\0')
return true;
if (digitChar != '0')
return false;
}

return true;
}

void MultiplyAdd(ref Span<int> currentBuffer, uint multiplier, uint addValue)
{
value = -value;
Span<int> curBits = currentBuffer.Slice(0, currentBufferSize);
uint carry = addValue;

for (int i = 0; i < curBits.Length; i++)
{
ulong p = (ulong)multiplier * (uint)curBits[i] + carry;
curBits[i] = (int)p;
carry = (uint)(p >> 32);
}

if (carry == 0)
return;

if (currentBufferSize == currentBuffer.Length)
{
int[]? arrayToReturn = arrayFromPool;

arrayFromPool = ArrayPool<int>.Shared.Rent(checked(currentBufferSize * 2));
currentBuffer.CopyTo(arrayFromPool);
currentBuffer = arrayFromPool;

if (arrayToReturn != null)
ArrayPool<int>.Shared.Return(arrayToReturn);
}

currentBuffer[currentBufferSize] = (int)carry;
currentBufferSize++;
}
return true;
}

// This function is consistent with VM\COMNumber.cpp!COMNumber::ParseFormatSpecifier
Expand Down