Open PetSerAl opened 1 year ago
Tagging subscribers to this area: @dotnet/area-system-linq See info in area-owners.md if you want to be subscribed.
Author: | PetSerAl |
---|---|
Assignees: | - |
Labels: | `area-System.Linq`, `untriaged` |
Milestone: | - |
Talked with @tannergooding about this. We're leaning toward keeping the new behavior that works well with vectorized implementations. For consistency across configurations though (such as when intrinsics are disabled), the naive implementation would need to be updated. Its new behavior would be to avoid the overflow exceptions through upcasting until the final result is achieved (and checked against the result type). If we stick with the previous behavior, our ability to vectorize this method is substantially hindered.
Assuming the inconsistency is unacceptable, our .NET 8 resolution options are:
@stephentoub -- what do you think?
@PetSerAl, what is the scenario where this is actually causing problems for you?
I have no scenario, which is currently affected. But I find no discussion of that change in PR. And I decide to raise issue explicitly. So conscious decision about it can be made. And we at least have proper documentation of expected behavior, and any discrepancies between vectorized/non-vectorized versions, if allowed.
I wouldn't say this is an issue specific to vectorization, you can encounter similar behaviour in .NET 7 simply by shuffling the source enumerable:
Test1().Sum(); // 0
Test1().Order().Sum(); // OverflowException
TL;DR adding fixed-size integers isn't an associative operation.
I think we should just document this as a breaking change and consider adding a LINQ-specific feature switch if we do receive reports of users being impacted (I suspect that's going to be unlikely).
Added needs-breaking-change-doc-created
label because this issue has the breaking-change
label.
needs-breaking-change-doc-created
label.Tagging @dotnet/compat for awareness of the breaking change.
@eiriktsarpalis, the consideration here isn't whether there is a difference between [ int.MaxValue, -1, +1 ].Sum()
and [ int.MaxValue, +1, -1].Sum()
(although that is an interesting case to consider as well)
The consideration is that there is a difference for [ int.MaxValue, +1, -1].Sum()
based on the hardware you run on and our behavior is now non-deterministic (which is justifiably very bad and can lead to very hard to track down bugs).
We basically have a few options we could do...
We say this break is by design and document the new behavior. -- I personally think this is the worst option because, as I indicated above, it can lead to very hard to track down bugs and bugs that only repro some of the time and only on some hardware.
We say this break is not by design and no longer vectorize the code. -- I think this is an "ok" option, but it means we will never be able to provide perf gains here.
We say this break is desirable and update the algorithms to only throw if the total sum of integers overflows. This would require a change to both the scalar and vector algorithms to sum themselves in the next largest integer when an overflow is first detected. So something like:
public static int Sum(ReadOnlySpan<int> values)
{
int sum = 0;
int lastSign = 0;
for (int i = 0; i < values.Length; i++)
{
int tmp = sum + value;
int sign = value >>> 31;
// ideally this check is recognized and optimized by the JIT
if ((lastSign == sign) && (sign != (tmp >>> 31)))
{
return SumWithPotentialOverflow(sum, values.Slice(i));
}
}
return sum;
}
private static int SumWithPotentialOverflow(long sum, ReadOnlySpan
return checked((int)sum); }
Under such a setup we will only throw an `OverflowException` if the total sum of all values results in an overflow and therefore the ordering of them doesn't matter. It also avoids requiring upcasts for every value processed and allows vectorization to be kept.
The one niche consideration that still exists is if someone has defined a custom enumerator that yields more than `int.MaxValue` results for 32-bit sums and more than `long.MaxValue` results for 64-bit sums. That could still overflow, but is likely so rare that it's acceptable to leave as undefined (rather than continuing to fallback to a larger temporary).
I've marked the PR which made this change as breaking - we will need to document that if the break remains. We should make sure that has a workaround for folks. Moving this to track follow up work (option 3) to 9.0.0. We could always do option 2 in servicing if we get more feedback around this.
My suggestion would be to fallback to sequential summing, when overflow encountered. That will make sure everything, that can be summed sequential, will not overflow with vectorization.
int Fallback(ref int ptr, nuint length, Vector<int> accumulator, nuint index, nuint lastKnownGood) {
while(index > lastKnownGood) {
index -= (nuint)Vector<int>.Count;
accumulator -= Vector.LoadUnsafe(ref ptr, index);
}
long wideResult = 0;
for(int i = 0; i < Vector<int>.Count; ++i) {
wideResult += accumulator[i];
}
int result = checked((int)wideResult);
for(; index < length; ++index) {
checked { result += Unsafe.Add(ref ptr, index); }
}
return result;
}
I removed the regression-from-last-release
label to indicate that this change is already present in .NET 8
Description
84519 introduce vectorization into LINQ
Sum
forint
andlong
. Vectorization change the order in which number summed. In certain cases this affect wheneverOverflowException
will be thrown.Reproduction Steps
Expected behavior
Actual behavior
Regression?
No response
Known Workarounds
No response
Configuration
No response
Other information
No response