Skip to content

Improve perf of Enumerable.Sum/Average/Max/Min for arrays and lists - #64624

Merged
stephentoub merged 2 commits into
dotnet:mainfrom
stephentoub:morelinqvector
Feb 4, 2022
Merged

Improve perf of Enumerable.Sum/Average/Max/Min for arrays and lists#64624
stephentoub merged 2 commits into
dotnet:mainfrom
stephentoub:morelinqvector

Conversation

@stephentoub

Copy link
Copy Markdown
Member

It's common to use these terminal functions for quick stats on arrays and lists of values. Just the overhead of enumerating as an enumerable (involving multiple interface dispatch) per iteration is significant, and it's much faster to directly enumerate the contents of the array or the list. In some cases, we can further use vectorization to speed up the processing.

This change:

  • Adds a helper that does a fast check to see if it can extract a span from an enumerable that's actually an array or a list. It could be augmented to detect other interesting types, but T[] and List<T> are the most relevant from the data I've seen, and we can fairly quickly do type checks to get the most benefit for a small amount of cost.
  • Uses that helper in the int/long/float/double/decimal overloads of Sum/Average/Min/Max to add a span-based path.
  • Vectorizes Sum for float and double
  • Vectorizes Average for int, float, and double (the latter two via use of Sum)

@tannergooding, I assume the use of vectorization for floats/doubles could change the answer in some cases due to lack of associativity, yes? Thoughts on how much we should care about that? Also, it seemed like it should be possible to vectorize some of the methods doing checked arithmetic, but I wasn't sure how to do so correctly and skipped those. It also seemed like it should be possible to vectorize min/max for floats/doubles, but they have special-handling of NaN I couldn't figure out how to replicate with Vector.

@eiriktsarpalis, please let me know in general if you're ok with the extra code here for this special-casing. We don't have to do it, but it seems like an inexpensive win for what appears to be common, e.g. building up a List<int> and calling Average/Sum/Min/Max on it. There are certainly other patterns common with these, e.g. calling Min on the result of a Select or on other collection types. We could subsequently choose to also special-case IList<T> in order to save an interface dispatch per invocation, though I'm hopeful we'll get most of that for free with dynamic PGO, and we could subsequently choose to special-case the internal partitioning interfaces; the difficulty with those is the check for whether the type implements them is more expensive, and there's some aspect of diminishing returns because you're already doing more work to compute each element (whereas with arrays / lists, the amount of work per element is tiny).

MethodToolchainLengthModeMeanErrorRatio
MinFloat\main\corerun.exe2Array20.379 ns0.4283 ns1.00
MinFloat\pr\corerun.exe2Array4.493 ns0.1502 ns0.23
MinDecimal\main\corerun.exe2Array22.809 ns0.2779 ns1.00
MinDecimal\pr\corerun.exe2Array8.633 ns0.1527 ns0.38
SumInt32\main\corerun.exe2Array18.643 ns0.3083 ns1.00
SumInt32\pr\corerun.exe2Array1.924 ns0.0544 ns0.10
SumInt64\main\corerun.exe2Array18.344 ns0.1885 ns1.00
SumInt64\pr\corerun.exe2Array1.972 ns0.0360 ns0.11
SumFloat\main\corerun.exe2Array19.220 ns0.3734 ns1.00
SumFloat\pr\corerun.exe2Array4.102 ns0.0312 ns0.21
SumDecimal\main\corerun.exe2Array27.653 ns0.5665 ns1.00
SumDecimal\pr\corerun.exe2Array13.084 ns0.1069 ns0.47
AverageInt32\main\corerun.exe2Array18.634 ns0.3122 ns1.00
AverageInt32\pr\corerun.exe2Array5.071 ns0.1270 ns0.27
AverageInt64\main\corerun.exe2Array18.582 ns0.3613 ns1.00
AverageInt64\pr\corerun.exe2Array3.473 ns0.0440 ns0.19
AverageFloat\main\corerun.exe2Array19.145 ns0.2944 ns1.00
AverageFloat\pr\corerun.exe2Array4.278 ns0.0346 ns0.22
AverageDecimal\main\corerun.exe2Array55.346 ns0.7298 ns1.00
AverageDecimal\pr\corerun.exe2Array49.087 ns0.1350 ns0.89
MinFloat\main\corerun.exe2Enumerable28.121 ns0.4956 ns1.00
MinFloat\pr\corerun.exe2Enumerable27.229 ns0.4659 ns0.97
MinDecimal\main\corerun.exe2Enumerable33.184 ns0.3598 ns1.00
MinDecimal\pr\corerun.exe2Enumerable34.396 ns0.6804 ns1.04
SumInt32\main\corerun.exe2Enumerable21.329 ns0.2865 ns1.00
SumInt32\pr\corerun.exe2Enumerable21.540 ns0.2581 ns1.01
SumInt64\main\corerun.exe2Enumerable24.865 ns0.2726 ns1.00
SumInt64\pr\corerun.exe2Enumerable25.556 ns0.4184 ns1.03
SumFloat\main\corerun.exe2Enumerable25.950 ns0.2292 ns1.00
SumFloat\pr\corerun.exe2Enumerable26.443 ns0.3911 ns1.02
SumDecimal\main\corerun.exe2Enumerable37.731 ns0.6087 ns1.00
SumDecimal\pr\corerun.exe2Enumerable38.357 ns0.7728 ns1.02
AverageInt32\main\corerun.exe2Enumerable21.104 ns0.2414 ns1.00
AverageInt32\pr\corerun.exe2Enumerable22.065 ns0.4544 ns1.05
AverageInt64\main\corerun.exe2Enumerable24.994 ns0.5023 ns1.00
AverageInt64\pr\corerun.exe2Enumerable26.308 ns0.5447 ns1.05
AverageFloat\main\corerun.exe2Enumerable27.288 ns0.5206 ns1.00
AverageFloat\pr\corerun.exe2Enumerable26.597 ns0.4992 ns0.97
AverageDecimal\main\corerun.exe2Enumerable67.316 ns0.4518 ns1.00
AverageDecimal\pr\corerun.exe2Enumerable80.256 ns0.3487 ns1.19
MinFloat\main\corerun.exe32Array162.367 ns1.4179 ns1.00
MinFloat\pr\corerun.exe32Array40.095 ns0.5829 ns0.25
MinDecimal\main\corerun.exe32Array268.355 ns3.1338 ns1.00
MinDecimal\pr\corerun.exe32Array172.761 ns1.5649 ns0.64
SumInt32\main\corerun.exe32Array138.323 ns1.1075 ns1.00
SumInt32\pr\corerun.exe32Array13.147 ns0.1089 ns0.10
SumInt64\main\corerun.exe32Array144.375 ns1.6040 ns1.00
SumInt64\pr\corerun.exe32Array12.756 ns0.2747 ns0.09
SumFloat\main\corerun.exe32Array142.810 ns0.6627 ns1.00
SumFloat\pr\corerun.exe32Array8.980 ns0.0286 ns0.06
SumDecimal\main\corerun.exe32Array268.380 ns5.2945 ns1.00
SumDecimal\pr\corerun.exe32Array158.555 ns2.0546 ns0.59
AverageInt32\main\corerun.exe32Array154.934 ns2.9998 ns1.00
AverageInt32\pr\corerun.exe32Array10.516 ns0.1869 ns0.07
AverageInt64\main\corerun.exe32Array148.826 ns2.9388 ns1.00
AverageInt64\pr\corerun.exe32Array14.362 ns0.3140 ns0.10
AverageFloat\main\corerun.exe32Array146.798 ns2.5321 ns1.00
AverageFloat\pr\corerun.exe32Array9.836 ns0.1240 ns0.07
AverageDecimal\main\corerun.exe32Array353.931 ns3.2577 ns1.00
AverageDecimal\pr\corerun.exe32Array173.172 ns3.3340 ns0.49
MinFloat\main\corerun.exe32Enumerable219.598 ns3.2260 ns1.00
MinFloat\pr\corerun.exe32Enumerable201.268 ns2.5158 ns0.92
MinDecimal\main\corerun.exe32Enumerable401.314 ns6.9631 ns1.00
MinDecimal\pr\corerun.exe32Enumerable396.958 ns4.2611 ns0.99
SumInt32\main\corerun.exe32Enumerable130.008 ns2.4007 ns1.00
SumInt32\pr\corerun.exe32Enumerable133.748 ns2.4726 ns1.03
SumInt64\main\corerun.exe32Enumerable176.430 ns3.4085 ns1.00
SumInt64\pr\corerun.exe32Enumerable175.259 ns3.0756 ns0.99
SumFloat\main\corerun.exe32Enumerable195.938 ns2.9538 ns1.00
SumFloat\pr\corerun.exe32Enumerable190.320 ns3.0678 ns0.97
SumDecimal\main\corerun.exe32Enumerable382.905 ns3.4056 ns1.00
SumDecimal\pr\corerun.exe32Enumerable384.330 ns4.6222 ns1.00
AverageInt32\main\corerun.exe32Enumerable132.928 ns2.5226 ns1.00
AverageInt32\pr\corerun.exe32Enumerable143.767 ns2.4111 ns1.08
AverageInt64\main\corerun.exe32Enumerable181.207 ns1.6801 ns1.00
AverageInt64\pr\corerun.exe32Enumerable177.174 ns2.1069 ns0.98
AverageFloat\main\corerun.exe32Enumerable203.800 ns2.0559 ns1.00
AverageFloat\pr\corerun.exe32Enumerable201.102 ns3.2647 ns0.99
AverageDecimal\main\corerun.exe32Enumerable497.159 ns1.4778 ns1.00
AverageDecimal\pr\corerun.exe32Enumerable492.052 ns2.3135 ns0.99
usingSystem.Collections.Generic;usingSystem.Linq;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;publicclassProgram{publicstaticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);[Params(2,32)]publicintLength{get;set;}[Params("Array","Enumerable")]publicstringMode{get;set;}privateIEnumerable<int>_ints;privateIEnumerable<long>_longs;privateIEnumerable<float>_floats;privateIEnumerable<decimal>_decimals;[Benchmark]publicfloatMinFloat()=>_floats.Min();[Benchmark]publicintSumInt32()=>_ints.Sum();[Benchmark]publiclongSumInt64()=>_longs.Sum();[Benchmark]publicfloatSumFloat()=>_floats.Sum();[Benchmark]publicdecimalSumDecimal()=>_decimals.Sum();[Benchmark]publicdoubleAverageInt32()=>_ints.Average();[Benchmark]publicdoubleAverageInt64()=>_longs.Average();[Benchmark]publicfloatAverageFloat()=>_floats.Average();[Benchmark]publicdecimalAverageDecimal()=>_decimals.Average();[GlobalSetup]publicvoidSetup(){_ints=Enumerable.Range(1,Length);if(Mode=="Array")_ints=_ints.ToArray();_longs=Enumerable.Range(1,Length).Select(i =>(long)i);if(Mode=="Array")_longs=_longs.ToArray();_floats=Enumerable.Range(1,Length).Select(i =>(float)i);if(Mode=="Array")_floats=_floats.ToArray();_decimals=Enumerable.Range(1,Length).Select(i =>(decimal)i);if(Mode=="Array")_decimals=_decimals.ToArray();}}

@ghost

ghost commented Feb 1, 2022

Copy link
Copy Markdown

Tagging subscribers to this area: @dotnet/area-system-linq
See info in area-owners.md if you want to be subscribed.

Issue Details

It's common to use these terminal functions for quick stats on arrays and lists of values. Just the overhead of enumerating as an enumerable (involving multiple interface dispatch) per iteration is significant, and it's much faster to directly enumerate the contents of the array or the list. In some cases, we can further use vectorization to speed up the processing.

This change:

  • Adds a helper that does a fast check to see if it can extract a span from an enumerable that's actually an array or a list. It could be augmented to detect other interesting types, but T[] and List<T> are the most relevant from the data I've seen, and we can fairly quickly do type checks to get the most benefit for a small amount of cost.
  • Uses that helper in the int/long/float/double/decimal overloads of Sum/Average/Min/Max to add a span-based path.
  • Vectorizes Sum for float and double
  • Vectorizes Average for int, float, and double (the latter two via use of Sum)

@tannergooding, I assume the use of vectorization for floats/doubles could change the answer in some cases due to lack of associativity, yes? Thoughts on how much we should care about that? Also, it seemed like it should be possible to vectorize some of the methods doing checked arithmetic, but I wasn't sure how to do so correctly and skipped those. It also seemed like it should be possible to vectorize min/max for floats/doubles, but they have special-handling of NaN I couldn't figure out how to replicate with Vector.

@eiriktsarpalis, please let me know in general if you're ok with the extra code here for this special-casing. We don't have to do it, but it seems like an inexpensive win for what appears to be common, e.g. building up a List<int> and calling Average/Sum/Min/Max on it. There are certainly other patterns common with these, e.g. calling Min on the result of a Select or on other collection types. We could subsequently choose to also special-case IList<T> in order to save an interface dispatch per invocation, though I'm hopeful we'll get most of that for free with dynamic PGO, and we could subsequently choose to special-case the internal partitioning interfaces; the difficulty with those is the check for whether the type implements them is more expensive, and there's some aspect of diminishing returns because you're already doing more work to compute each element (whereas with arrays / lists, the amount of work per element is tiny).

MethodToolchainLengthModeMeanErrorRatio
MinFloat\main\corerun.exe2Array20.379 ns0.4283 ns1.00
MinFloat\pr\corerun.exe2Array4.493 ns0.1502 ns0.23
MinDecimal\main\corerun.exe2Array22.809 ns0.2779 ns1.00
MinDecimal\pr\corerun.exe2Array8.633 ns0.1527 ns0.38
SumInt32\main\corerun.exe2Array18.643 ns0.3083 ns1.00
SumInt32\pr\corerun.exe2Array1.924 ns0.0544 ns0.10
SumInt64\main\corerun.exe2Array18.344 ns0.1885 ns1.00
SumInt64\pr\corerun.exe2Array1.972 ns0.0360 ns0.11
SumFloat\main\corerun.exe2Array19.220 ns0.3734 ns1.00
SumFloat\pr\corerun.exe2Array4.102 ns0.0312 ns0.21
SumDecimal\main\corerun.exe2Array27.653 ns0.5665 ns1.00
SumDecimal\pr\corerun.exe2Array13.084 ns0.1069 ns0.47
AverageInt32\main\corerun.exe2Array18.634 ns0.3122 ns1.00
AverageInt32\pr\corerun.exe2Array5.071 ns0.1270 ns0.27
AverageInt64\main\corerun.exe2Array18.582 ns0.3613 ns1.00
AverageInt64\pr\corerun.exe2Array3.473 ns0.0440 ns0.19
AverageFloat\main\corerun.exe2Array19.145 ns0.2944 ns1.00
AverageFloat\pr\corerun.exe2Array4.278 ns0.0346 ns0.22
AverageDecimal\main\corerun.exe2Array55.346 ns0.7298 ns1.00
AverageDecimal\pr\corerun.exe2Array49.087 ns0.1350 ns0.89
MinFloat\main\corerun.exe2Enumerable28.121 ns0.4956 ns1.00
MinFloat\pr\corerun.exe2Enumerable27.229 ns0.4659 ns0.97
MinDecimal\main\corerun.exe2Enumerable33.184 ns0.3598 ns1.00
MinDecimal\pr\corerun.exe2Enumerable34.396 ns0.6804 ns1.04
SumInt32\main\corerun.exe2Enumerable21.329 ns0.2865 ns1.00
SumInt32\pr\corerun.exe2Enumerable21.540 ns0.2581 ns1.01
SumInt64\main\corerun.exe2Enumerable24.865 ns0.2726 ns1.00
SumInt64\pr\corerun.exe2Enumerable25.556 ns0.4184 ns1.03
SumFloat\main\corerun.exe2Enumerable25.950 ns0.2292 ns1.00
SumFloat\pr\corerun.exe2Enumerable26.443 ns0.3911 ns1.02
SumDecimal\main\corerun.exe2Enumerable37.731 ns0.6087 ns1.00
SumDecimal\pr\corerun.exe2Enumerable38.357 ns0.7728 ns1.02
AverageInt32\main\corerun.exe2Enumerable21.104 ns0.2414 ns1.00
AverageInt32\pr\corerun.exe2Enumerable22.065 ns0.4544 ns1.05
AverageInt64\main\corerun.exe2Enumerable24.994 ns0.5023 ns1.00
AverageInt64\pr\corerun.exe2Enumerable26.308 ns0.5447 ns1.05
AverageFloat\main\corerun.exe2Enumerable27.288 ns0.5206 ns1.00
AverageFloat\pr\corerun.exe2Enumerable26.597 ns0.4992 ns0.97
AverageDecimal\main\corerun.exe2Enumerable67.316 ns0.4518 ns1.00
AverageDecimal\pr\corerun.exe2Enumerable80.256 ns0.3487 ns1.19
MinFloat\main\corerun.exe32Array162.367 ns1.4179 ns1.00
MinFloat\pr\corerun.exe32Array40.095 ns0.5829 ns0.25
MinDecimal\main\corerun.exe32Array268.355 ns3.1338 ns1.00
MinDecimal\pr\corerun.exe32Array172.761 ns1.5649 ns0.64
SumInt32\main\corerun.exe32Array138.323 ns1.1075 ns1.00
SumInt32\pr\corerun.exe32Array13.147 ns0.1089 ns0.10
SumInt64\main\corerun.exe32Array144.375 ns1.6040 ns1.00
SumInt64\pr\corerun.exe32Array12.756 ns0.2747 ns0.09
SumFloat\main\corerun.exe32Array142.810 ns0.6627 ns1.00
SumFloat\pr\corerun.exe32Array8.980 ns0.0286 ns0.06
SumDecimal\main\corerun.exe32Array268.380 ns5.2945 ns1.00
SumDecimal\pr\corerun.exe32Array158.555 ns2.0546 ns0.59
AverageInt32\main\corerun.exe32Array154.934 ns2.9998 ns1.00
AverageInt32\pr\corerun.exe32Array10.516 ns0.1869 ns0.07
AverageInt64\main\corerun.exe32Array148.826 ns2.9388 ns1.00
AverageInt64\pr\corerun.exe32Array14.362 ns0.3140 ns0.10
AverageFloat\main\corerun.exe32Array146.798 ns2.5321 ns1.00
AverageFloat\pr\corerun.exe32Array9.836 ns0.1240 ns0.07
AverageDecimal\main\corerun.exe32Array353.931 ns3.2577 ns1.00
AverageDecimal\pr\corerun.exe32Array173.172 ns3.3340 ns0.49
MinFloat\main\corerun.exe32Enumerable219.598 ns3.2260 ns1.00
MinFloat\pr\corerun.exe32Enumerable201.268 ns2.5158 ns0.92
MinDecimal\main\corerun.exe32Enumerable401.314 ns6.9631 ns1.00
MinDecimal\pr\corerun.exe32Enumerable396.958 ns4.2611 ns0.99
SumInt32\main\corerun.exe32Enumerable130.008 ns2.4007 ns1.00
SumInt32\pr\corerun.exe32Enumerable133.748 ns2.4726 ns1.03
SumInt64\main\corerun.exe32Enumerable176.430 ns3.4085 ns1.00
SumInt64\pr\corerun.exe32Enumerable175.259 ns3.0756 ns0.99
SumFloat\main\corerun.exe32Enumerable195.938 ns2.9538 ns1.00
SumFloat\pr\corerun.exe32Enumerable190.320 ns3.0678 ns0.97
SumDecimal\main\corerun.exe32Enumerable382.905 ns3.4056 ns1.00
SumDecimal\pr\corerun.exe32Enumerable384.330 ns4.6222 ns1.00
AverageInt32\main\corerun.exe32Enumerable132.928 ns2.5226 ns1.00
AverageInt32\pr\corerun.exe32Enumerable143.767 ns2.4111 ns1.08
AverageInt64\main\corerun.exe32Enumerable181.207 ns1.6801 ns1.00
AverageInt64\pr\corerun.exe32Enumerable177.174 ns2.1069 ns0.98
AverageFloat\main\corerun.exe32Enumerable203.800 ns2.0559 ns1.00
AverageFloat\pr\corerun.exe32Enumerable201.102 ns3.2647 ns0.99
AverageDecimal\main\corerun.exe32Enumerable497.159 ns1.4778 ns1.00
AverageDecimal\pr\corerun.exe32Enumerable492.052 ns2.3135 ns0.99
usingSystem.Collections.Generic;usingSystem.Linq;usingBenchmarkDotNet.Attributes;usingBenchmarkDotNet.Running;publicclassProgram{publicstaticvoidMain(string[]args)=>BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);[Params(2,32)]publicintLength{get;set;}[Params("Array","Enumerable")]publicstringMode{get;set;}privateIEnumerable<int>_ints;privateIEnumerable<long>_longs;privateIEnumerable<float>_floats;privateIEnumerable<decimal>_decimals;[Benchmark]publicfloatMinFloat()=>_floats.Min();[Benchmark]publicintSumInt32()=>_ints.Sum();[Benchmark]publiclongSumInt64()=>_longs.Sum();[Benchmark]publicfloatSumFloat()=>_floats.Sum();[Benchmark]publicdecimalSumDecimal()=>_decimals.Sum();[Benchmark]publicdoubleAverageInt32()=>_ints.Average();[Benchmark]publicdoubleAverageInt64()=>_longs.Average();[Benchmark]publicfloatAverageFloat()=>_floats.Average();[Benchmark]publicdecimalAverageDecimal()=>_decimals.Average();[GlobalSetup]publicvoidSetup(){_ints=Enumerable.Range(1,Length);if(Mode=="Array")_ints=_ints.ToArray();_longs=Enumerable.Range(1,Length).Select(i =>(long)i);if(Mode=="Array")_longs=_longs.ToArray();_floats=Enumerable.Range(1,Length).Select(i =>(float)i);if(Mode=="Array")_floats=_floats.ToArray();_decimals=Enumerable.Range(1,Length).Select(i =>(decimal)i);if(Mode=="Array")_decimals=_decimals.ToArray();}}
Author:stephentoub
Assignees:-
Labels:

area-System.Linq, tenet-performance

Milestone:-

Comment threadsrc/libraries/System.Linq/src/System/Linq/Enumerable.cs Outdated
@eiriktsarpalis

Copy link
Copy Markdown
Member

@eiriktsarpalis, please let me know in general if you're ok with the extra code here for this special-casing.

Should be ok, but per your own comment in #64470 (comment) it would be nice if the code be moved out of Linq eventually.

Comment threadsrc/libraries/System.Linq/src/System/Linq/Average.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Average.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Sum.cs Outdated
Comment threadsrc/libraries/System.Linq/src/System/Linq/Sum.cs Outdated
@tannergooding

tannergooding commented Feb 1, 2022

Copy link
Copy Markdown
Member

I assume the use of vectorization for floats/doubles could change the answer in some cases due to lack of associativity, yes? Thoughts on how much we should care about that? Also, it seemed like it should be possible to vectorize some of the methods doing checked arithmetic, but I wasn't sure how to do so correctly and skipped those. It also seemed like it should be possible to vectorize min/max for floats/doubles, but they have special-handling of NaN I couldn't figure out how to replicate with Vector.

@stephentoub right. There are a number of cases where we cannot trivially vectorize float/double (particularly operations that compute a new value like Sum) because it can drastically change the output and will lead to determinism issues among other bugs (put another way, I think we should care a lot and not do this for float/double).

The simplest scenario to explain is that with integers the delta between one representable value and the next is always 1 (ignoring overflow for a minute). Where-as with floating-point this starts at epsilon (the smallest representable value greater than zero) and then doubles every power of 2 after that. For float this means that for 2^22 to 2^23 the delta between values is 0.5, for 2^23 to 2^24 the delta between values is 1, for 2^24 to 2^25 it is 2, at 2^26 it is 4, and so on (in both directions going down, including to negative powers until you hit epsilon and going up until you hit float.MaxValue).

What this means is that if you take a case like new float[] { 16777216.0f, 0.5f, 0.5f, 0.5f } you will get back 16777216.0f because 2^24 + 0.5 always returns 2^24 since its less than the delta between values (2). However if you reorder this to new float[] { 0.5f, 0.5f, 0.5f, 16777216.0f } you instead get back 16777218.0f, because 0.5 + 0.5 +0.5 is enough to create 1.5 and then (due to rounding) is enough to round up to 16777218.

Vectorization is impacted here because it ends up adding n to n + Count and then adding them "across" at the end. There have been open asks for several years to provide a "fast math" switch that would allow such optimizations but its non-trivial to achieve. The easiest thing would be to expose a new overload that allows users to explicitly opt-into such differences.


Things that do not compute a "new value" like Min/Max (it only does a comparison and returns one of the inputs) can be vectorized but it needs to take into account things like NaN handling as you indicated.

For overflow checking, it can be simple when you know the inputs are always positive or negative. It's not as simple when it can randomly be positive or negative.

@stephentoub

Copy link
Copy Markdown
MemberAuthor

I assume the use of vectorization for floats/doubles could change the answer in some cases due to lack of associativity, yes? Thoughts on how much we should care about that?

I think we should care a lot and not do this for float/double

Ok, deleting.

Comment threadsrc/libraries/System.Linq/src/System/Linq/Max.cs Outdated
It's very common to use these terminal functions for quick stats on arrays and lists of values. Just the overhead of enumerating as an enumerable (involving multiple interface dispatch) per iteration is significant, and it's much faster to directly enumerate the contents of the array or the list. In some cases, we can further use vectorization to speed up the processing.
This change:
- Adds a helper that does a fast check to see if it can extract a span from an enumerable that's actually an array or a list. It could be augmented to detect other interesting types, but `T[]` and `List<T>` are the most relevant from the data I've seen, and we can fairly quickly do type checks to get the most benefit for a small amount of cost.
- Uses that helper in the int/long/float/double/decimal overloads of Sum/Average/Min/Max to add a span-based path.
- Vectorizes Sum for float and double
- Vectorizes Average for int, float, and double (the latter two via use of Sum)
Vector<long> sums = default;
do
{
Vector.Widen(new Vector<int>(span.Slice(i)), out Vector<long> low, out Vector<long> high);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the bounds checks actually getting elided here for new Vector<int>(span.Slice(i))? This seems like its going to be doing a bunch of extra work each iteration

@stephentoubstephentoubFeb 4, 2022

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe so. But I also didn't want to introduce unsafe code. Is there a way to write it that's "safe" (i.e. if I made a mistake it would result in an exception rather than potential corruption / security problems) and that avoids the bounds checks? My assumption is even if I eliminate the checks in Slice by using a loop pattern the JIT recognizes, Vector itself will still be doing a length check on the length of the span supplied, and that won't be removed (or will it)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some other places in the runtime use a helper built around Unsafe.ReadUnaligned and then the xplat helpers now have Vector128.LoadUnsafe(ref T, nuint index).

There's never really going to be a "safe" way to do this unless the JIT gets special support, however. You functionally have some T and want to read 2-32 of that T (depending on what the actual type is). So the best you'll generally get is doing the right checks up front and potentially adding some asserts. Otherwise, you pay the cost of slicing, copying the span, and doing the relevant bounds checks each iteration.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with that in this code. If these helpers are ever moved into MemoryExtensions, we can go to town on optimizing the heck out of them, both with avoiding bounds checks and with adding whatever additional paths are necessary to get the best perf. For LINQ, I think this is good enough, and there's benefit to no proliferating unsafe code here.

@ghostghost locked as resolved and limited conversation to collaborators Mar 7, 2022
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Linqtenet-performancePerformance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@stephentoub@eiriktsarpalis@tannergooding@gfoidl@danmoseley