Enumerable.Range(0, X).ToList() performance improvements in .NET 8




Date Added (UTC):

08 Apr 2024 @ 02:06

Date Updated (UTC):

08 Apr 2024 @ 02:06


.NET Version(s):

.NET 7 .NET 8

Tag(s):

#.Net8PerfImprovement #Collections #LINQ


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



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

[Config(typeof(Config))]
[HideColumns("Error", "StdDev", "Median", "RatioSD", "EnvironmentVariables", "Runtime")]
public class RangeToListBenchmark
{
    [Benchmark]
    public List<int> RangeToList() => Enumerable.Range(0, 16_384).ToList();

    private class Config : ManualConfig
    {
        public Config()
        {
            AddJob(Job.Default.WithId(".NET 7").WithRuntime(CoreRuntime.Core70).AsBaseline());
            AddJob(Job.Default.WithId(".NET 8").WithRuntime(CoreRuntime.Core80));

            SummaryStyle =
                SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
        }
    }
}

// .NET 7, .NET 8
public List<int> RangeToList()
{
    return Enumerable.ToList(Enumerable.Range(0, 16384));
}

// .NET 7
.method public hidebysig 
    instance class [System.Collections]System.Collections.Generic.List`1<int32> RangeToList () cil managed 
{
    .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
        01 00 01 00 00
    )
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 19 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x208e
    // Code size 17 (0x11)
    .maxstack 8

    // sequence point: (line 26, col 39) to (line 26, col 75) in _
    IL_0000: ldc.i4.0
    IL_0001: ldc.i4 16384
    IL_0006: call class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> [System.Linq]System.Linq.Enumerable::Range(int32, int32)
    IL_000b: call class [System.Collections]System.Collections.Generic.List`1<!!0> [System.Linq]System.Linq.Enumerable::ToList<int32>(class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0>)
    IL_0010: ret
}

// .NET 8
.method public hidebysig 
    instance class [System.Collections]System.Collections.Generic.List`1<int32> RangeToList () cil managed 
{
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
        01 00 01 00 00
    )
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 19 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2050
    // Code size 17 (0x11)
    .maxstack 8

    // sequence point: (line 26, col 39) to (line 26, col 75) in _
    IL_0000: ldc.i4.0
    IL_0001: ldc.i4 16384
    IL_0006: call class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> [System.Linq]System.Linq.Enumerable::Range(int32, int32)
    IL_000b: call class [System.Collections]System.Collections.Generic.List`1<!!0> [System.Linq]System.Linq.Enumerable::ToList<int32>(class [System.Runtime]System.Collections.Generic.IEnumerable`1<!!0>)
    IL_0010: ret
}

// .NET 7 (X64)
RangeToList()
    L0000: push rsi
    L0001: sub rsp, 0x20
    L0005: mov rcx, 0x7ffbd3d76660
    L000f: call 0x00007ffc2e19ec40
    L0014: mov rsi, rax
    L0017: call System.Environment.get_CurrentManagedThreadId()
    L001c: mov [rsi+8], eax
    L001f: xor ecx, ecx
    L0021: mov [rsi+0x14], ecx
    L0024: mov dword ptr [rsi+0x18], 0x4000
    L002b: mov rcx, rsi
    L002e: add rsp, 0x20
    L0032: pop rsi
    L0033: jmp qword ptr [0x7ffbd584b0c0]
// .NET 8 (X64)
RangeToList()
    L0000: push rbx
    L0001: sub rsp, 0x20
    L0005: mov rcx, 0x7ff9d66886f8
    L000f: call 0x00007ffa3009ae10
    L0014: mov rbx, rax
    L0017: call System.Environment.get_CurrentManagedThreadId()
    L001c: mov [rbx+8], eax
    L001f: xor ecx, ecx
    L0021: mov [rbx+0x14], ecx
    L0024: mov dword ptr [rbx+0x18], 0x4000
    L002b: mov rcx, rbx
    L002e: add rsp, 0x20
    L0032: pop rbx
    L0033: jmp qword ptr [0x7ff9d820eef8]


Benchmark Description:


[PR](https://github.com/dotnet/runtime/pull/87992)

The provided benchmark code is designed to measure the performance of converting a range of integers to a list using the `Enumerable.Range` method followed by the `ToList` method in .NET. This benchmark is configured to run on two different versions of the .NET runtime, specifically .NET 7 and .NET 8, allowing for a comparison of performance across these versions. The configuration and setup for this benchmark are specified using attributes and a custom configuration class that extends `BenchmarkDotNet`'s `ManualConfig`. ### General Setup - **.NET Versions:** The benchmark is explicitly set up to run on .NET 7 and .NET 8, allowing for direct performance comparisons between these two versions. This is achieved by adding two jobs in the configuration, each specifying the runtime version. - **BenchmarkDotNet:** This is a powerful library for benchmarking .NET code, providing detailed performance metrics. It's used here to define, run, and report the benchmarks. - **Configuration:** The custom configuration class `Config` extends `ManualConfig` from BenchmarkDotNet, specifying the jobs (runtime versions) and customizing the summary style. The summary style is set to display ratios in percentage form, making it easier to understand the performance differences between .NET versions. - **Columns:** The benchmark class is annotated to hide certain columns in the output (`Error`, `StdDev`, `Median`, `RatioSD`, `EnvironmentVariables`, `Runtime`), focusing the report on the most relevant metrics for this test. ### Benchmark Method: `RangeToList` - **Purpose:** This method is designed to measure the performance of generating a sequence of integers using `Enumerable.Range` and then converting this sequence to a `List<int>` using the `ToList` method. This is a common operation in .NET applications where a range of values needs to be materialized into a list for further processing. - **Performance Aspect:** The benchmark tests the efficiency of the `Range` and `ToList` methods, including how quickly the range can be generated and converted to a list, and the memory allocations involved in this process. This can provide insights into the overhead of such operations in .NET applications. - **Why It's Important:** Understanding the performance of converting ranges to lists is crucial for optimizing applications that perform this operation frequently, especially when dealing with large ranges. It can impact both the speed and memory usage of an application. - **Expected Results/Insights:** By running this benchmark, you can expect to gain insights into: - The performance difference in executing the `RangeToList` operation between .NET 7 and .NET 8, helping to identify if newer versions of the runtime offer optimizations for such operations. - The overall efficiency of creating lists from ranges in terms of execution time and memory allocation, which can inform decisions about using this pattern in performance-critical parts of an application. In summary, this benchmark is a targeted test designed to measure and compare the performance of a specific operation (`Enumerable.Range` followed by `ToList`) across different .NET runtime versions. The insights gained can guide optimizations and inform decisions about application architecture and coding patterns.


Benchmark Comments: