diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 2ca6c7a1bf5ab3..59fef1a6d5da65 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -27,6 +27,15 @@ public readonly struct BigInteger internal const int kcbitUlong = 64; internal const int DecimalScaleFactorMask = 0x00FF0000; + // Various APIs only allow up to int.MaxValue bits, so we will restrict ourselves + // to fit within this given our underlying storage representation and the maximum + // array length. This gives us just shy of 256MB as the largest allocation size. + // + // Such a value allows for almost 646,456,974 digits, which is more than large enough + // for typical scenarios. If user code requires more than this, they should likely + // roll their own type that utilizes native memory and other specialized techniques. + internal static int MaxLength => Array.MaxLength / kcbitUint; + // For values int.MinValue < n <= int.MaxValue, the value is stored in sign // and _bits is null. For all other values, sign is +1 or -1 and the bits are in _bits internal readonly int _sign; // Do not rename (binary serialization) @@ -487,22 +496,22 @@ internal BigInteger(int n, uint[]? rgu) /// The bool indicating the sign of the value. private BigInteger(ReadOnlySpan value, bool negative) { + // Try to conserve space as much as possible by checking for wasted leading span entries + // sometimes the span has leading zeros from bit manipulation operations & and ^ + + int length = value.LastIndexOfAnyExcept(0u) + 1; + value = value[..length]; + if (value.Length > MaxLength) { ThrowHelper.ThrowOverflowException(); } - int len; - - // Try to conserve space as much as possible by checking for wasted leading span entries - // sometimes the span has leading zeros from bit manipulation operations & and ^ - for (len = value.Length; len > 0 && value[len - 1] == 0; len--); - - if (len == 0) + if (value.Length == 0) { - this = s_bnZeroInt; + this = default; } - else if (len == 1 && value[0] < kuMaskHighBit) + else if (value.Length == 1 && value[0] < kuMaskHighBit) { // Values like (Int32.MaxValue+1) are stored as "0x80000000" and as such cannot be packed into _sign _sign = negative ? -(int)value[0] : (int)value[0]; @@ -516,7 +525,7 @@ private BigInteger(ReadOnlySpan value, bool negative) else { _sign = negative ? -1 : +1; - _bits = value.Slice(0, len).ToArray(); + _bits = value.ToArray(); } AssertValid(); } @@ -527,87 +536,88 @@ private BigInteger(ReadOnlySpan value, bool negative) /// private BigInteger(Span value) { + bool isNegative; + int length; + + if ((value.Length > 0) && ((int)value[^1] < 0)) + { + isNegative = true; + length = value.LastIndexOfAnyExcept(uint.MaxValue) + 1; + + if ((length == 0) || ((int)value[length - 1] > 0)) + { + // We ne need to preserve the sign bit + length++; + } + Debug.Assert((int)value[length - 1] < 0); + } + else + { + isNegative = false; + length = value.LastIndexOfAnyExcept(0u) + 1; + } + value = value[..length]; + if (value.Length > MaxLength) { ThrowHelper.ThrowOverflowException(); } - int dwordCount = value.Length; - bool isNegative = dwordCount > 0 && ((value[dwordCount - 1] & kuMaskHighBit) == kuMaskHighBit); - - // Try to conserve space as much as possible by checking for wasted leading span entries - while (dwordCount > 0 && value[dwordCount - 1] == 0) dwordCount--; - - if (dwordCount == 0) + if (value.Length == 0) { - // BigInteger.Zero + // 0 this = s_bnZeroInt; - AssertValid(); - return; } - if (dwordCount == 1) + else if (value.Length == 1) { - if (unchecked((int)value[0]) < 0 && !isNegative) + if (isNegative) { - _bits = new uint[1]; - _bits[0] = value[0]; - _sign = +1; + if (value[0] == uint.MaxValue) + { + // -1 + this = s_bnMinusOneInt; + } + else if (value[0] == kuMaskHighBit) + { + // int.MinValue + this = s_bnMinInt; + } + else + { + _sign = unchecked((int)value[0]); + _bits = null; + } } - // Handle the special cases where the BigInteger likely fits into _sign - else if (int.MinValue == unchecked((int)value[0])) + else if (unchecked((int)value[0]) < 0) { - this = s_bnMinInt; + _sign = +1; + _bits = [value[0]]; } else { _sign = unchecked((int)value[0]); _bits = null; } - AssertValid(); - return; } - - if (!isNegative) + else { - // Handle the simple positive value cases where the input is already in sign magnitude - _sign = +1; - value = value.Slice(0, dwordCount); - _bits = value.ToArray(); - AssertValid(); - return; - } - - // Finally handle the more complex cases where we must transform the input into sign magnitude - NumericsHelpers.DangerousMakeTwosComplement(value); // mutates val + if (isNegative) + { + NumericsHelpers.DangerousMakeTwosComplement(value); - // Pack _bits to remove any wasted space after the twos complement - int len = value.Length; - while (len > 0 && value[len - 1] == 0) len--; + // Retrim any leading zeros carried from the sign + length = value.LastIndexOfAnyExcept(0u) + 1; + value = value[..length]; - // The number is represented by a single dword - if (len == 1 && unchecked((int)(value[0])) > 0) - { - if (value[0] == 1 /* abs(-1) */) - { - this = s_bnMinusOneInt; - } - else if (value[0] == kuMaskHighBit /* abs(Int32.MinValue) */) - { - this = s_bnMinInt; + _sign = -1; } else { - _sign = (-1) * ((int)value[0]); - _bits = null; + _sign = +1; } - } - else - { - _sign = -1; - _bits = value.Slice(0, len).ToArray(); + _bits = value.ToArray(); } AssertValid(); - return; } public static BigInteger Zero { get { return s_bnZeroInt; } } @@ -616,8 +626,6 @@ private BigInteger(Span value) public static BigInteger MinusOne { get { return s_bnMinusOneInt; } } - internal static int MaxLength => Array.MaxLength / sizeof(uint); - public bool IsPowerOfTwo { get diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/op_rightshift.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/op_rightshift.cs index da6ac2615b9c1b..404902f61972e7 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigInteger/op_rightshift.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigInteger/op_rightshift.cs @@ -29,19 +29,21 @@ public static void BigShiftsTest() public static void LargeNegativeBigIntegerShiftTest() { // Create a very large negative BigInteger - BigInteger bigInt = new BigInteger(-1) << int.MaxValue; - Assert.Equal(2147483647, bigInt.GetBitLength()); + int bitsPerElement = 8 * sizeof(uint); + int maxBitLength = ((Array.MaxLength / bitsPerElement) * bitsPerElement); + BigInteger bigInt = new BigInteger(-1) << (maxBitLength - 1); + Assert.Equal(maxBitLength - 1, bigInt.GetBitLength()); Assert.Equal(-1, bigInt.Sign); // Validate internal representation. - // At this point, bigInt should be a 1 followed by int.MaxValue zeros. + // At this point, bigInt should be a 1 followed by maxBitLength - 1 zeros. // Given this, bigInt._bits is expected to be structured as follows: - // - _bits.Length == (int.MaxValue + 1) / (8 * sizeof(uint)) + // - _bits.Length == ceil(maxBitLength / bitsPerElement) // - First (_bits.Length - 1) elements: 0x00000000 // - Last element: 0x80000000 // ^------ (There's the leading '1') - Assert.Equal(((uint)int.MaxValue + 1) / (8 * sizeof(uint)), (uint)bigInt._bits.Length); + Assert.Equal((maxBitLength + (bitsPerElement - 1)) / bitsPerElement, bigInt._bits.Length); uint i = 0; for (; i < (bigInt._bits.Length - 1); i++) { @@ -52,18 +54,18 @@ public static void LargeNegativeBigIntegerShiftTest() // Right shift the BigInteger BigInteger shiftedBigInt = bigInt >> 1; - Assert.Equal(2147483646, shiftedBigInt.GetBitLength()); + Assert.Equal(maxBitLength - 2, shiftedBigInt.GetBitLength()); Assert.Equal(-1, shiftedBigInt.Sign); // Validate internal representation. - // At this point, shiftedBigInt should be a 1 followed by int.MaxValue - 1 zeros. + // At this point, shiftedBigInt should be a 1 followed by maxBitLength - 2 zeros. // Given this, shiftedBigInt._bits is expected to be structured as follows: - // - _bits.Length == (int.MaxValue + 1) / (8 * sizeof(uint)) + // - _bits.Length == ceil((maxBitLength - 1) / bitsPerElement) // - First (_bits.Length - 1) elements: 0x00000000 // - Last element: 0x40000000 // ^------ (the '1' is now one position to the right) - Assert.Equal(((uint)int.MaxValue + 1) / (8 * sizeof(uint)), (uint)shiftedBigInt._bits.Length); + Assert.Equal(((maxBitLength - 1) + (bitsPerElement - 1)) / bitsPerElement, shiftedBigInt._bits.Length); i = 0; for (; i < (shiftedBigInt._bits.Length - 1); i++) {