Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Number.Parsing.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Number.Rounding.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\BFloat16.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\BigIntegerCalculator.Shared.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\BitOperations.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\Colors\Argb.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Numerics\Colors\Argb.T.cs" />
Expand Down
1,001 changes: 485 additions & 516 deletions src/libraries/System.Private.CoreLib/src/System/Number.BigInteger.cs

Large diffs are not rendered by default.

116 changes: 93 additions & 23 deletions src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,23 +276,23 @@ private static uint Dragon4(ulong mantissa, int exponent, uint mantissaHighBitId
// Output the exponent of the first digit we will print
decimalExponent = --digitExponent;

// In preparation for calling BigInteger.HeuristicDivie(), we need to scale up our values such that the highest block of the denominator is greater than or equal to 8.
// In preparation for calling BigInteger.HeuristicDivide(), we need to scale up our values such that the highest block of the denominator is greater than or equal to 8.
// We also need to guarantee that the numerator can never have a length greater than the denominator after each loop iteration.
// This requires the highest block of the denominator to be less than or equal to 429496729 which is the highest number that can be multiplied by 10 without overflowing to a new block.
// This requires the highest block of the denominator to be less than or equal to the highest number that can be multiplied by 10 without overflowing to a new block (nuint.MaxValue / 10).

Debug.Assert(scale.GetLength() > 0);
uint hiBlock = scale.GetBlock(scale.GetLength() - 1);
nuint hiBlock = scale.GetBlock(scale.GetLength() - 1);

if ((hiBlock < 8) || (hiBlock > 429496729))
if ((hiBlock < 8) || (hiBlock > (nuint.MaxValue / 10)))
{
// Perform a bit shift on all values to get the highest block of the denominator into the range [8,429496729].
// We are more likely to make accurate quotient estimations in BigInteger.HeuristicDivide() with higher denominator values so we shift the denominator to place the highest bit at index 27 of the highest block.
// This is safe because (2^28 - 1) = 268435455 which is less than 429496729.
// This means that all values with a highest bit at index 27 are within range.
// Perform a bit shift on all values to get the highest block of the denominator into the range [8, nuint.MaxValue / 10].
// We are more likely to make accurate quotient estimations in BigInteger.HeuristicDivide() with higher denominator values so we shift the denominator to place the highest bit at index (BitsPerBlock - 5) of the highest block.
// This is safe because (2^(BitsPerBlock - 4) - 1) is less than (nuint.MaxValue / 10).
// This means that all values with a highest bit at index (BitsPerBlock - 5) are within range.
Debug.Assert(hiBlock != 0);
int hiBlockLog2 = BitOperations.Log2(hiBlock);
Debug.Assert((hiBlockLog2 < 3) || (hiBlockLog2 > 27));
int shift = (32 + 27 - hiBlockLog2) % 32;
Debug.Assert((hiBlockLog2 < 3) || (hiBlockLog2 > (BigInteger.BitsPerBlock - 5)));
int shift = (BigInteger.BitsPerBlock + (BigInteger.BitsPerBlock - 5) - hiBlockLog2) % BigInteger.BitsPerBlock;

scale.ShiftLeft(shift);
scaledValue.ShiftLeft(shift);
Expand All @@ -307,7 +307,7 @@ private static uint Dragon4(ulong mantissa, int exponent, uint mantissaHighBitId
// These values are used to inspect why the print loop terminated so we can properly round the final digit.
bool low; // did the value get within marginLow distance from zero
bool high; // did the value get within marginHigh distance from one
uint outputDigit; // current digit being output
uint outputDigit = 0; // current digit being output

if (cutoffNumber == -1)
{
Expand Down Expand Up @@ -370,24 +370,94 @@ private static uint Dragon4(ulong mantissa, int exponent, uint mantissaHighBitId
low = false;
high = false;

while (true)
// This path never inspects per-digit margins, so when the per-digit divide is genuinely
// O(L > 1) we pull a whole native block's worth of digits at a time: `floor(scaledValue *
// 10^(K-1) / scale)` yields the next K digits in one big divide. K is the largest power of
// ten that fits a block (19 on 64-bit, 9 on 32-bit). The digit at cutoffExponent is always
// the rounding digit, so it is left to a per-digit tail whose leftover scaledValue drives
// the rounding logic. A single-block scale (e.g. small integers) already extracts a digit
// in O(1), so batching would only add block-divide overhead -- it stays on the scalar loop.
if (scale.GetLength() > 1)
{
// divide out the scale to extract the digit
outputDigit = BigInteger.HeuristicDivide(ref scaledValue, ref scale);
Debug.Assert(outputDigit < 10);
int maxBatchDigits = (nint.Size == 8) ? 19 : 9;
bool roundingDigitExtracted = false;

if (scaledValue.IsZero() || (digitExponent <= cutoffExponent))
while (digitExponent > cutoffExponent)
{
break;
int batchDigits = Math.Min(digitExponent - cutoffExponent, maxBatchDigits);

if (batchDigits > 1)
{
scaledValue.MultiplyPow10((uint)(batchDigits - 1));
}

BigInteger.DivRem(ref scaledValue, ref scale, out BigInteger batch, out BigInteger remainder);
Debug.Assert(batch.GetLength() <= 1);

// Expand the block quotient into `batchDigits` zero-padded decimal digits.
nuint blockDigits = (batch.GetLength() == 0) ? 0 : batch.GetBlock(0);

for (int i = batchDigits - 1; i >= 0; i--)
{
(blockDigits, nuint digit) = Math.DivRem(blockDigits, (nuint)10);
buffer[curDigit + i] = (byte)('0' + digit);
}

if (remainder.IsZero())
{
// The value was captured exactly within this block. The trailing zeros are not
// significant, so the last non-zero digit becomes the rounding digit and the
// leftover scaledValue is exactly zero (an exact round-down below).
int last = curDigit + batchDigits - 1;

while ((last > curDigit) && (buffer[last] == '0'))
{
last--;
}

outputDigit = (uint)(buffer[last] - '0');
curDigit = last;
BigInteger.SetZero(out scaledValue);
roundingDigitExtracted = true;
break;
}

curDigit += batchDigits;

// multiply larger by the output base to line up the next block
remainder.Multiply10();
scaledValue = remainder;
digitExponent -= batchDigits;
}

// store the output digit
buffer[curDigit] = (byte)('0' + outputDigit);
curDigit++;
if (!roundingDigitExtracted)
{
// extract the rounding digit at cutoffExponent
outputDigit = BigInteger.HeuristicDivide(ref scaledValue, ref scale);
Debug.Assert(outputDigit < 10);
}
}
else
{
while (true)
{
// divide out the scale to extract the digit
outputDigit = BigInteger.HeuristicDivide(ref scaledValue, ref scale);
Debug.Assert(outputDigit < 10);

// multiply larger by the output base
scaledValue.Multiply10();
digitExponent--;
if (scaledValue.IsZero() || (digitExponent <= cutoffExponent))
{
break;
}

// store the output digit
buffer[curDigit] = (byte)('0' + outputDigit);
curDigit++;

// multiply larger by the output base
scaledValue.Multiply10();
digitExponent--;
}
}
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,8 +706,22 @@ internal static void AccumulateDecimalDigitsIntoBigInteger(scoped ref NumberBuff

while (remaining != 0)
{
uint count = Math.Min(remaining, 9);
uint value = DigitsToUInt32(src, (int)(count));
// Batch as many digits as fill a single block -- 9 on 32-bit (10^9 fits a uint),
// 19 on 64-bit (10^19 fits a nuint) -- so wide builds halve the multiply/add
// iterations instead of staying 32-bit-granular. nint.Size constant-folds here.
uint count;
nuint value;

if (nint.Size == 8)
{
count = Math.Min(remaining, 19);
value = (nuint)DigitsToUInt64(src, (int)count);
}
else
{
count = Math.Min(remaining, 9);
value = DigitsToUInt32(src, (int)count);
}

result.MultiplyPow10(count);
result.Add(value);
Expand Down Expand Up @@ -847,45 +861,12 @@ private static ulong ConvertBigIntegerToFloatingPointBits<TFloat>(ref BigInteger
return AssembleFloatingPointBits<TFloat>(value.ToUInt64(), baseExponent, !hasNonZeroFractionalPart);
}

(int topBlockIndex, int topBlockBits) = Math.DivRem(integerBitsOfPrecision, 32);
int middleBlockIndex = topBlockIndex - 1;
int bottomBlockIndex = middleBlockIndex - 1;
// The mantissa is the top 64 bits of the value; everything below that window is the tail.
int windowBitIndex = integerBitsOfPrecision - 64;

ulong mantissa;
int exponent = baseExponent + ((int)(bottomBlockIndex) * 32);
bool hasZeroTail = !hasNonZeroFractionalPart;

// When the top 64-bits perfectly span two blocks, we can get those blocks directly
if (topBlockBits == 0)
{
mantissa = ((ulong)(value.GetBlock(middleBlockIndex)) << 32) + value.GetBlock(bottomBlockIndex);
}
else
{
// Otherwise, we need to read three blocks and combine them into a 64-bit mantissa

int bottomBlockShift = (int)(topBlockBits);
int topBlockShift = 64 - bottomBlockShift;
int middleBlockShift = topBlockShift - 32;

exponent += (int)(topBlockBits);

uint bottomBlock = value.GetBlock(bottomBlockIndex);
uint bottomBits = bottomBlock >> bottomBlockShift;

ulong middleBits = (ulong)(value.GetBlock(middleBlockIndex)) << middleBlockShift;
ulong topBits = (ulong)(value.GetBlock(topBlockIndex)) << topBlockShift;

mantissa = topBits + middleBits + bottomBits;

uint unusedBottomBlockBitsMask = (1u << (int)(topBlockBits)) - 1;
hasZeroTail &= (bottomBlock & unusedBottomBlockBitsMask) == 0;
}

for (int i = 0; i < bottomBlockIndex; i++)
{
hasZeroTail &= (value.GetBlock(i) == 0);
}
ulong mantissa = value.GetBits64(windowBitIndex);
int exponent = baseExponent + windowBitIndex;
bool hasZeroTail = !hasNonZeroFractionalPart && value.HasZeroTail(windowBitIndex);

return AssembleFloatingPointBits<TFloat>(mantissa, exponent, hasZeroTail);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ public static unsafe TNumber RoundToDecimalDigits<TNumber>(TNumber value, int di
do
{
BigInteger.DivRem(ref quotient, ref ten, out BigInteger newQuotient, out BigInteger digit);
uint digitValue = digit.IsZero() ? 0 : digit.GetBlock(0);
uint digitValue = digit.IsZero() ? 0 : (uint)digit.GetBlock(0);
buffer[digitCount++] = (byte)('0' + digitValue);
BigInteger.SetValue(out quotient, ref newQuotient);
}
Expand Down
Loading
Loading