-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Updating BigInteger to implement IBinaryInteger and ISignedNumber #68964
Updating BigInteger to implement IBinaryInteger and ISignedNumber #68964
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @dotnet/area-system-numerics Issue Detailsnull
|
for (int i = 0; i < value._bits.Length; i++) | ||
{ | ||
uint part = value._bits[i]; | ||
result += uint.PopCount(part); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be worth reinterpreting _bits as ulongs and doing a PopCount on each of those (plus any additional uint that remains)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would probably be better to use nuint
(so 32-bit perf stays good), but there is a more general rewrite of BigInteger
planned at some point and I think it would be better to do that then rather than complicate the current logic more.
- The general goal of the rewrite is to make
BigInteger
usenuint[]
(rather thanuint[]
) and make it two's complement, rather than one's complement representation
int byteCount = (value._bits is null) ? sizeof(int) : (value._bits.Length * 4); | ||
|
||
// Normalize the rotate amount to drop full rotations | ||
rotateAmount %= (int)(byteCount * 8L); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this guaranteed to handle overflow (negative byteCount) correctly? Some very limited testing says yes, but I wondered if you knew if this was a solid guarantee.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe so, although its possible there is some edge case.
More concretely we have APIs that allow getting the underlying bytes into an array or span, and so long term I want to limit _bits.Length
to be no more than Array.MaxLength
bytes. This ensures you can always fit the underlying data into a span and 2^2_147_483_591
should be more than big enoguh for anyone.
The shifting and rotate APIs are then somewhat "oddities" in that they currently take int
but theoretically we allow up to Array.MaxLength * 8
bits, which is currently up to 17,179,868,728
bits (requiring up to 34 bits).
If we limit the actual value to no more than int.MaxLength
bits so these APIs continue to make sense then we allow BigInteger
up to 268_435_455
bytes or approximately 256 MB. This allows an astronomically large number of approx: 7.15 * 10^80_807_123
.
I'm not necessarily "hardset" on either of these limits, but I think they are practical and realistic for .NET. Elsewise we need "chunking" APIs and various APIs may throw or not function as expected in all scenarios.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added handling to ensure that _bits.Length <= (Array.Length / sizeof(uint))
and to throw an OverflowException
otherwise.
Changed the modulus logic to then be rotateAmount = (int)(rotateAmount % (byteCount * 8L))
} | ||
|
||
/// <inheritdoc cref="IBinaryInteger{TSelf}.LeadingZeroCount(TSelf)" /> | ||
public static BigInteger LeadingZeroCount(BigInteger value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you change BigInteger
to use nint[] instead of int[] in a future PR (per your comment at https://github.com/dotnet/runtime/pull/68964/files#r866933097), you'll need to normalize the result of this method across platforms. Otherwise the results could differ on x86 vs x64.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. However, I believe that's fine/acceptable.
Various types, including user defined ones, may be variable sized. As such, LeadingZeroCount
is logically a value between 0
and GetByteCount() * 8L
(put another way it is (GetByteCount() * 8L) - GetShortestBitLength()
.
This is functionally similar to how nint
, nuint
, CLong
, and CULong
work as well and gives a well-defined contract even in the face of this. The value is then still usable within these limits/expectations.
{ | ||
// When the value is positive, we simply need to copy all bits as little endian | ||
|
||
ref byte address = ref MemoryMarshal.GetReference(destination); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for GetReference
in the common little-endian case. If the length check on line 3308 above succeeds, you know that MemoryMarshal.Cast<uint, byte>(bits).CopyTo(destination)
will also succeed. The only time you'd need to bswap is in the big-endian case.
Once your two's-complement implementation comes online, you'll be able to reuse the same memcpy code there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to intentionally keep this simple for now and handle it as part of the rewrite.
src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs
Outdated
Show resolved
Hide resolved
} | ||
|
||
/// <inheritdoc cref="INumber{TSelf}.MaxMagnitude(TSelf, TSelf)" /> | ||
public static BigInteger MaxMagnitude(BigInteger x, BigInteger y) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perf: This method could end up allocating as part of the temporary -x and -y calculations, even though the results are discarded. Is this acceptable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BigInteger
, in general, allocates all over the place today. I'm not overly concerned about this but I definitely could simplify it a bit more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Only skimmed the test, but they look robust.
@@ -603,6 +621,8 @@ private BigInteger(Span<uint> value) | |||
|
|||
public static BigInteger MinusOne { get { return s_bnMinusOneInt; } } | |||
|
|||
internal static int MaxLength => Array.MaxLength / sizeof(uint); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this restriction designed so that we can hold every BigInteger in an array of bytes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and based on the feedback from API review we are going to restrict this further such that MaxLength => Array.MaxLength / (sizeof(uint) * 8)
That is, it is expected that any BigInteger
be no more than approx 2 billion bits
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per the summary I gave above (#68964 (comment))
This is up to 268_435_455 bytes
or approximately 256 MB
. This still allows an astronomically large number of approx: 7.15 * 10^80_807_123
at which point
Most conceivable code will fit well into 80_807_100
(80 million) digits provided and anything beyond is increasingly niche and starts having very complex considerations into how it impacts the GC, memory usage in general, etc.
@@ -1766,6 +1786,31 @@ private static BigInteger Subtract(ReadOnlySpan<uint> leftBits, int leftSign, Re | |||
return new BigInteger(value); | |||
} | |||
|
|||
public static implicit operator BigInteger(nint value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this operator
keyword referring to the cast operation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Most operators are of the form static TResult operator ...
where the ...
is the operator name (+
, -
, *
, /
, %
, etc; including the new checked variants checked +
, etc).
Implicit and explicit conversion operators take a different shape of the form static implicit operator TResult
and static explicit operator TResult
(with the checked
keyword still following the operator
keyword, so static explicit operator checked TResult
).
// | ||
|
||
/// <inheritdoc cref="IAdditionOperators{TSelf, TOther, TResult}.op_Addition(TSelf, TOther)" /> | ||
static BigInteger IAdditionOperators<BigInteger, BigInteger, BigInteger>.operator checked +(BigInteger left, BigInteger right) => left + right; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed now that checked +
has a DIM in IAdditionOperators
with this behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It won't be once the DIM support is available for use by the language. As of right now, its not in or usable at the moment so we still need to define this ourselves.
Hi @tannergooding, the new test is failing on big-endian s390x: Seems like there could be again endian issues? In particular, it seems odd that |
This is fixed in #69391. |
No description provided.