LINQ Min, Max, Sum and Average performance improvements for .NET 6 v .NET 7 v .NET 8




Date Added (UTC):

10 Apr 2024 @ 23:55

Date Updated (UTC):

02 May 2024 @ 12:39


.NET Version(s):

.NET 6 .NET 7 .NET 8

Tag(s):

#LINQ #.Net7PerfImprovement


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports; 

namespace BenchmarkDotNet.Samples
{
    [Config(typeof(Config))]
    [SimpleJob(RuntimeMoniker.Net60, baseline: true)]
    [SimpleJob(RuntimeMoniker.Net70)]
    [SimpleJob(RuntimeMoniker.Net80)]
    public class LINQBenchmarks
    {
        [Params(1000)]
        public int Length { get; set; }

        private IEnumerable<int> _source;

        [GlobalSetup]
        public void Setup() => _source = Enumerable.Range(1, Length).ToArray();

        [Benchmark]
        public int Min() => _source.Min();

        [Benchmark]
        public int Max() => _source.Max();

        [Benchmark]
        public float Sum() => _source.Sum();

        [Benchmark]
        public double Average() => _source.Average();

        private class Config : ManualConfig
        {
            public Config()
            {
                SummaryStyle =
                    SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
            }
        }
    }
}

// .NET 6, .NET 7, .NET 8
public int Min()
{
    return Enumerable.Min(_source);
}
// .NET 6, .NET 7, .NET 8
public int Max()
{
    return Enumerable.Max(_source);
}
// .NET 6, .NET 7, .NET 8
public float Sum()
{
    return Enumerable.Sum(_source);
}
// .NET 6, .NET 7, .NET 8
public double Average()
{
    return Enumerable.Average(_source);
}

// .NET 6
.method public hidebysig 
    instance int32 Min () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 24 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20a9
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 37, col 29) to (line 37, col 42) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Min(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}

// .NET 7
.method public hidebysig 
    instance int32 Min () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 24 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20b8
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 37, col 29) to (line 37, col 42) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Min(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}

// .NET 8
.method public hidebysig 
    instance int32 Min () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 24 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x207a
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 37, col 29) to (line 37, col 42) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Min(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}
// .NET 6
.method public hidebysig 
    instance int32 Max () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 27 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20b6
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 40, col 29) to (line 40, col 42) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Max(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}

// .NET 7
.method public hidebysig 
    instance int32 Max () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 27 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20c5
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 40, col 29) to (line 40, col 42) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Max(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}

// .NET 8
.method public hidebysig 
    instance int32 Max () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 27 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2087
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 40, col 29) to (line 40, col 42) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Max(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}
// .NET 6
.method public hidebysig 
    instance float32 Sum () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20c3
    // Code size 13 (0xd)
    .maxstack 8

    // sequence point: (line 43, col 31) to (line 43, col 44) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Sum(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: conv.r4
    IL_000c: ret
}

// .NET 7
.method public hidebysig 
    instance float32 Sum () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20d2
    // Code size 13 (0xd)
    .maxstack 8

    // sequence point: (line 43, col 31) to (line 43, col 44) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Sum(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: conv.r4
    IL_000c: ret
}

// .NET 8
.method public hidebysig 
    instance float32 Sum () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2094
    // Code size 13 (0xd)
    .maxstack 8

    // sequence point: (line 43, col 31) to (line 43, col 44) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call int32 [System.Linq]System.Linq.Enumerable::Sum(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: conv.r4
    IL_000c: ret
}
// .NET 6
.method public hidebysig 
    instance float64 Average () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2d 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20d1
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 46, col 36) to (line 46, col 53) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call float64 [System.Linq]System.Linq.Enumerable::Average(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}

// .NET 7
.method public hidebysig 
    instance float64 Average () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2d 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20e0
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 46, col 36) to (line 46, col 53) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call float64 [System.Linq]System.Linq.Enumerable::Average(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}

// .NET 8
.method public hidebysig 
    instance float64 Average () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2d 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20a2
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 46, col 36) to (line 46, col 53) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> BenchmarkDotNet.Samples.LINQBenchmarks::_source
    IL_0006: call float64 [System.Linq]System.Linq.Enumerable::Average(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>)
    IL_000b: ret
}

// .NET 6 (X64)
Min()
    L0000: mov rcx, [rcx+8]
    L0004: jmp System.Linq.Enumerable.Min(System.Collections.Generic.IEnumerable`1<Int32>)
// .NET 7 (X64)
Min()
    L0000: mov rcx, [rcx+8]
    L0004: jmp qword ptr [0x7ffd612971b0]
// .NET 8 (X64)
Min()
    L0000: mov rcx, [rcx+8]
    L0004: jmp qword ptr [0x7ffb47415938]
// .NET 6 (X64)
Max()
    L0000: mov rcx, [rcx+8]
    L0004: jmp System.Linq.Enumerable.Max(System.Collections.Generic.IEnumerable`1<Int32>)
// .NET 7 (X64)
Max()
    L0000: mov rcx, [rcx+8]
    L0004: jmp qword ptr [0x7ffd612971c8]
// .NET 8 (X64)
Max()
    L0000: mov rcx, [rcx+8]
    L0004: jmp qword ptr [0x7ffb47416fd0]
// .NET 6 (X64)
Sum()
    L0000: sub rsp, 0x28
    L0004: vzeroupper
    L0007: mov rcx, [rcx+8]
    L000b: call System.Linq.Enumerable.Sum(System.Collections.Generic.IEnumerable`1<Int32>)
    L0010: vxorps xmm0, xmm0, xmm0
    L0014: vcvtsi2ss xmm0, xmm0, eax
    L0018: add rsp, 0x28
    L001c: ret
// .NET 7 (X64)
Sum()
    L0000: sub rsp, 0x28
    L0004: vzeroupper
    L0007: mov rcx, [rcx+8]
    L000b: call qword ptr [0x7ffd612971e0]
    L0011: vxorps xmm0, xmm0, xmm0
    L0015: vcvtsi2ss xmm0, xmm0, eax
    L0019: add rsp, 0x28
    L001d: ret
// .NET 8 (X64)
Sum()
    L0000: sub rsp, 0x28
    L0004: vzeroupper
    L0007: mov rcx, [rcx+8]
    L000b: call qword ptr [0x7ffb47416fe8]
    L0011: vxorps xmm0, xmm0, xmm0
    L0015: vcvtsi2ss xmm0, xmm0, eax
    L0019: add rsp, 0x28
    L001d: ret
// .NET 6 (X64)
Average()
    L0000: mov rcx, [rcx+8]
    L0004: jmp System.Linq.Enumerable.Average(System.Collections.Generic.IEnumerable`1<Int32>)
// .NET 7 (X64)
Average()
    L0000: mov rcx, [rcx+8]
    L0004: jmp qword ptr [0x7ffd5b4ab678]
// .NET 8 (X64)
Average()
    L0000: mov rcx, [rcx+8]
    L0004: jmp qword ptr [0x7ffb40d279d8]


Benchmark Description:


There was huge performance improvement from .NET 6 to .NET 7 for a number of LINQ methods such as Min, Max, Sum and Average. .NET 8 may have been marginal improved over 7 too.

The provided benchmark code is designed to measure the performance of various LINQ operations in .NET. It is set up to run on three different .NET runtime versions: .NET 6.0 (as the baseline), .NET 7.0, and a hypothetical .NET 8.0, allowing for performance comparisons across these versions. The benchmarks focus on four common LINQ methods: `Min()`, `Max()`, `Sum()`, and `Average()`, using an array of integers as the data source. ### General Setup - **.NET Versions:** The benchmarks are configured to run on .NET 6.0 (as the baseline), .NET 7.0, and a hypothetical future version, .NET 8.0. This setup allows for performance comparison across different .NET versions. - **Configuration:** A custom configuration class (`Config`) is defined to customize the summary style of the benchmark results, specifically to display ratio styles as percentages. - **Data Source:** The benchmarks operate on an array of integers generated from a sequence of numbers ranging from 1 to a specified length (`Length`), which is set to 1000 in this case. This setup ensures that each benchmark method works with the same dataset, providing a consistent basis for comparison. - **BenchmarkDotNet Attributes:** Various attributes are used to configure the benchmarks, including `[Config]` for specifying the custom configuration, `[SimpleJob]` for defining the runtime environments, and `[Params]` for setting the length of the data source. ### Benchmark Methods 1. **Min()** - **Purpose:** Measures the performance of finding the minimum value in the dataset. - **Importance:** The `Min()` operation is widely used in data analysis and processing scenarios. Understanding its performance characteristics helps in optimizing applications that require finding minimum values in large datasets. - **Expected Insights:** This benchmark will reveal how efficiently the `Min()` method can iterate over the dataset and identify the minimum value, and how this efficiency may vary across different .NET versions. 2. **Max()** - **Purpose:** Measures the performance of finding the maximum value in the dataset. - **Importance:** Similar to `Min()`, the `Max()` operation is crucial in various data processing contexts. Benchmarking this method provides insights into the overhead involved in scanning large datasets for the maximum value. - **Expected Insights:** The results will indicate the speed at which the `Max()` method can traverse the dataset and determine the maximum value, highlighting any performance improvements or regressions in newer .NET versions. 3. **Sum()** - **Purpose:** Measures the performance of calculating the sum of all values in the dataset. - **Importance:** Summation is a fundamental operation in numerical computing and data analysis. Benchmarking the `Sum()` method helps in understanding the efficiency of aggregate computations over large datasets. - **Expected Insights:** This benchmark will show how quickly the `Sum()` method can aggregate the data, providing a measure of its computational efficiency and how it scales with .NET versions. 4. **Average()** - **Purpose:** Measures the performance of computing the average value of the dataset. - **Importance:** Computing averages is another common operation in data analysis. Performance insights for the `Average()` method are valuable for optimizing applications that require frequent calculation of mean values. - **Expected Insights:** The benchmark will reveal the computational cost of calculating the average, including both the summation and division operations, across different .NET versions. ### Conclusion Running these benchmarks will provide valuable insights into the performance of basic LINQ operations across different .NET runtime versions. Users can expect to understand how the efficiency of these operations evolves with the .NET platform, helping them make informed decisions about which .NET version to target for optimal performance in their applications.


Benchmark Comments: