Conversation
Average(IEnumerable<int>) widens each vector of Int32 values and adds both halves into a single Vector<long>, so the two adds of an iteration and the adds of consecutive iterations all wait for each other, leaving the loop bound by add latency rather than throughput. Give each half its own accumulator and process two vectors per iteration, for four accumulators in total, combined once the loop ends. Widened Int32 values sum exactly in Int64 and integer addition is associative, so the result is unchanged regardless of how the partial sums are grouped. Measured on arm64 with a 128-bit Vector<int>: 2.0x at 1K elements, 2.4x at 8K and 2.6x at 1M, with no regression at 128 elements.
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @dotnet/area-system-linq |
|
The description says I could not run the System.Linq suite locally; that is no longer true. Built the runtime and ran |
|
Measured end to end through the public API rather than through an extracted loop, since that is how the runtime team runs benchmarks and it turned out to matter for my other PRs. Method: a console app calling
In the same run the APIs this branch does not touch — Tests with that build: |
Average(IEnumerable<int>)widens each vector ofInt32values and adds both halves into the same accumulator:Both adds in an iteration, and the adds of consecutive iterations, form one dependency chain, so the loop runs at the latency of the add rather than at its throughput.
Change
Each half gets its own accumulator and two vectors are processed per iteration, giving four accumulators that are combined once the loop ends. A second loop handles a leftover vector, and the scalar tail is unchanged.
Widened
Int32values sum exactly inInt64— no rounding, and no overflow, since a span of at mostint.MaxValueelements each below 2³¹ cannot exceed 2⁶² — and integer addition is associative, so grouping the partial sums differently cannot change the result. This is why the same change is not appropriate for the floating-point overloads.Measurements
Standalone harness (I could not build the runtime on this machine), Apple M-series arm64 where
Vector<int>.Countis 4. Minimum of 7 rounds, ns per call:I measured the two-accumulator variant as well, which is the smaller change, but four is consistently better and is what the PR implements. On hardware with a wider
Vector<T>the crossover length will be proportionally higher; the short-span path is unchanged and 128 elements already showed no regression here.Correctness
Differential against a scalar oracle in the harness: 60,000 cases, lengths 1–200, values seeded with
int.MinValue,int.MaxValueand zero alongside random ones — 0 mismatches. That covers lengths below one vector, exact multiples of one and two vectors, and every remainder.I have not run the System.Linq test suite locally for the reason above, so CI is the first full validation.