-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Fix BigInteger.Rotate{Left,Right}
#112864
Conversation
20bca5c
to
2ef4970
Compare
@@ -3239,267 +3238,89 @@ public static BigInteger PopCount(BigInteger value) | |||
public static BigInteger RotateLeft(BigInteger value, int rotateAmount) | |||
{ | |||
value.AssertValid(); | |||
int byteCount = (value._bits is null) ? sizeof(int) : (value._bits.Length * 4); |
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.
Can this initially be done as a smaller fix that doesn't rewrite the whole algorithm?
We likely need to backport this fix and a less complex change is more desirable for that, as it reduces risk.
A separate PR that also rewrites it for improved performance just for .NET 10 is fine, just ideally separate from the bug fix.
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.
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 don't think it should be backported.
- What is the correct definition on
RotateLeft
/RotateRight
ofBigInteger
?BigInteger
doesn't have a fixed bits length and doesn't use 2's complement code to store data. I'd rather document the behavior as undefined. - The issue is said to have existed since .NET 7. Who using an old version .NET would want this new 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.
This is functionally "by design".
There were two options possible, one of which always treated the bit sequence as the shortest possible two's complement representation and thus-1
is1
while+1
is01
. The other of which is to treat it in terms of the underlying storage unit. The latter was chosen as it was overall more consistent with existing behavior, especially when viewed across all possible types.
Originally posted by @tannergooding in #91169
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.
@skyoxZ, the backport isn't about changing the behavior; it's about fixing an edge case where the behavior is broken because RotateRight is effectively propagating the carry bit left instead of right in certain edge cases.
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 a user I can't predict the result of BigInteger.RotateLeft
so I don't care if it's a bug. I just expect the same input always leads to the same result and believe .NET team is unlikely to change it. A backport will break my expectation.
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.
The possible approaches for handling bitwise operations are as follows:
- Do not implement this operations. Throw an exception if called via an interface.
- Provide an explicit implementation based on a specific policy.
- Implement it as a public method based on a specific policy (chosen).
- Return an arbitrary, meaningless value (which is effectively the case for RotateRight).
Personally, I believe option 1 is the most desirable in an ideal scenario. An example of this approach is Java's BigInteger, which does not implement unsigned right shifts, stating that they "make little sense". @skyoxZ's argument seems to be based on the same idea.
However, changing to option 1 would be far too breaking and likely unfeasible.
@skyoxZ's stance (for the current version) aligns with option 4, advocating that breaking changes should be avoided. Breaking changes guideline - Bucket 2: Reasonable grey area
No one knows the specification, no one uses the method, and therefore no one would be affected even if its implementation has a bug.
On the other hand, even if no one knows the specification, it is reasonable to expect a public method to have a sensible implementation.
I will leave the final decision to .NET team.
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.
Personally, I believe option 1 is the most desirable in an ideal scenario
1 isn't done because it breaks user expectations of how operating with binary integers in generic contexts works. It also prevents doing common and sensible optimizations, like treating division by 2 as shift operation or being able to permute the underlying bits.
The behavior chosen for 3 is then "sensible" for singular rotations and it behaves in a well defined way. Which is that functionally ROL(x, amount)
will produce the same result as would happen for a standard integer x
stored with a precision of n-bits
. The biggest nuance is that ROR(ROL(x, amount), amount)
may not return x
since the stored precision might change and there's no way to track how many implied leading zero/sign bits were originally there.
The behavior is specifically that we determine the number of bits required for the "shortest" two's complement representation rounded up to the nearest 32-bits. We then rotate the number of bits specified. This produces a new BigInteger value and is therefore logically similar to had you manually done the two bit shifts as a single combined operation without increasing the backing storage. Because we produce a new BigInteger, that is then trimmed for storage and computation efficiency to remove unnecessary leading zero/sign bits, which is what can lead to not being able to recover the original 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.
Because its meant to behave in a well-defined way, you can fully predict the answer for a given integer x
. Because there is then a bug, it is applicable to apply a bug fix for the broken behavior especially as it is more likely to cause problems that it is returning an incorrect result.
66ecbda
to
ff68a43
Compare
@kzrnm were you not interested in taking the improvement here still, or was it no longer as meaningful with the other fix in? |
@tannergooding The original PR #112632 aimed to optimize all bitwise operations, so I included shift operations as well and submitted a new pull request #113005. |
Fix #112854
I reused the code from #112632.
Benchmark
Code