Creating lists with regular v collection expression approach in .NET 8

Date Added (UTC):

05 Apr 2024 @ 03:44

Date Updated (UTC):

05 Apr 2024 @ 03:44

.NET Version(s):

.NET 8



Added By:
Profile Image

.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;

[HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio)]
[ReturnValueValidator(failOnError: true)]
public class CollectionExpressions
    [Benchmark(Baseline = true)]
    public List<string> RegularList()
        return new List<string> { "apple", "banana", "orange" };

    public List<string> ColExpressionList()
        return ["apple", "banana", "orange"];

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

            SummaryStyle =

// .NET 8
public List<string> RegularList()
    List<string> list = new List<string>();
    return list;
// .NET 8
public List<string> ColExpressionList()
    List<string> list = new List<string>();
    CollectionsMarshal.SetCount(list, 3);
    Span<string> span = CollectionsMarshal.AsSpan(list);
    int num = 0;
    span[num] = "apple";
    span[num] = "banana";
    span[num] = "orange";
    return list;

// .NET 8
.method public hidebysig 
    instance class [System.Collections]System.Collections.Generic.List`1<string> RegularList () 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 01 00 54 02 08 42 61 73
        65 6c 69 6e 65 01
    // Method begins at RVA 0x2050
    // Code size 39 (0x27)
    .maxstack 8

    // sequence point: (line 28, col 9) to (line 28, col 65) in _
    IL_0000: newobj instance void class [System.Collections]System.Collections.Generic.List`1<string>::.ctor()
    IL_0005: dup
    IL_0006: ldstr "apple"
    IL_000b: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<string>::Add(!0)
    IL_0010: dup
    IL_0011: ldstr "banana"
    IL_0016: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<string>::Add(!0)
    IL_001b: dup
    IL_001c: ldstr "orange"
    IL_0021: callvirt instance void class [System.Collections]System.Collections.Generic.List`1<string>::Add(!0)
    IL_0026: ret
// .NET 8
.method public hidebysig 
    instance class [System.Collections]System.Collections.Generic.List`1<string> ColExpressionList () 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 1f 00 00 00 01 5f 00 00
    // Method begins at RVA 0x2078
    // Code size 76 (0x4c)
    .maxstack 3
    .locals init (
        [0] valuetype [System.Runtime]System.Span`1<string>,
        [1] int32

    // sequence point: (line 34, col 9) to (line 34, col 46) in _
    IL_0000: newobj instance void class [System.Collections]System.Collections.Generic.List`1<string>::.ctor()
    IL_0005: dup
    IL_0006: ldc.i4.3
    IL_0007: call void [System.Runtime.InteropServices]System.Runtime.InteropServices.CollectionsMarshal::SetCount<string>(class [System.Collections]System.Collections.Generic.List`1<!!0>, int32)
    IL_000c: dup
    IL_000d: call valuetype [System.Runtime]System.Span`1<!!0> [System.Runtime.InteropServices]System.Runtime.InteropServices.CollectionsMarshal::AsSpan<string>(class [System.Collections]System.Collections.Generic.List`1<!!0>)
    IL_0012: stloc.0
    IL_0013: ldc.i4.0
    IL_0014: stloc.1
    IL_0015: ldloca.s 0
    IL_0017: ldloc.1
    IL_0018: call instance !0& valuetype [System.Runtime]System.Span`1<string>::get_Item(int32)
    IL_001d: ldstr "apple"
    IL_0022: stind.ref
    IL_0023: ldloc.1
    IL_0024: ldc.i4.1
    IL_0025: add
    IL_0026: stloc.1
    IL_0027: ldloca.s 0
    IL_0029: ldloc.1
    IL_002a: call instance !0& valuetype [System.Runtime]System.Span`1<string>::get_Item(int32)
    IL_002f: ldstr "banana"
    IL_0034: stind.ref
    IL_0035: ldloc.1
    IL_0036: ldc.i4.1
    IL_0037: add
    IL_0038: stloc.1
    IL_0039: ldloca.s 0
    IL_003b: ldloc.1
    IL_003c: call instance !0& valuetype [System.Runtime]System.Span`1<string>::get_Item(int32)
    IL_0041: ldstr "orange"
    IL_0046: stind.ref
    IL_0047: ldloc.1
    IL_0048: ldc.i4.1
    IL_0049: add
    IL_004a: stloc.1
    IL_004b: ret

// .NET 8 (X64)
    L0000: push rbx
    L0001: sub rsp, 0x20
    L0005: mov rcx, 0x7fff054220e0
    L000f: call 0x00007fff64e3ae10
    L0014: mov rbx, rax
    L0017: mov rcx, 0x22c0c482818
    L0021: mov [rbx+8], rcx
    L0025: inc dword ptr [rbx+0x14]
    L0028: mov rcx, [rbx+8]
    L002c: mov edx, [rbx+0x10]
    L002f: mov eax, [rcx+8]
    L0032: cmp eax, edx
    L0034: jbe L00bf
    L003a: lea eax, [rdx+1]
    L003d: mov [rbx+0x10], eax
    L0040: mov edx, edx
    L0042: lea rcx, [rcx+rdx*8+0x10]
    L0047: mov rdx, 0x22a280654c8
    L0051: mov rdx, [rdx]
    L0054: call 0x00007fff64e3a680
    L0059: inc dword ptr [rbx+0x14]
    L005c: mov rcx, [rbx+8]
    L0060: mov edx, [rbx+0x10]
    L0063: mov eax, [rcx+8]
    L0066: cmp eax, edx
    L0068: jbe L00f6
    L006e: lea eax, [rdx+1]
    L0071: mov [rbx+0x10], eax
    L0074: mov edx, edx
    L0076: lea rcx, [rcx+rdx*8+0x10]
    L007b: mov rdx, 0x22a280654d0
    L0085: mov rdx, [rdx]
    L0088: call 0x00007fff64e3a680
    L008d: inc dword ptr [rbx+0x14]
    L0090: mov rcx, [rbx+8]
    L0094: mov edx, [rbx+0x10]
    L0097: mov eax, [rcx+8]
    L009a: cmp eax, edx
    L009c: jbe short L00d7
    L009e: lea eax, [rdx+1]
    L00a1: mov [rbx+0x10], eax
    L00a4: mov edx, edx
    L00a6: lea rcx, [rcx+rdx*8+0x10]
    L00ab: mov rdx, 0x22a280654d8
    L00b5: mov rdx, [rdx]
    L00b8: call 0x00007fff64e3a680
    L00bd: jmp short L00ed
    L00bf: mov rdx, 0x22a280654c8
    L00c9: mov rdx, [rdx]
    L00cc: mov rcx, rbx
    L00cf: call qword ptr [0x7fff0537e130]
    L00d5: jmp short L0059
    L00d7: mov rdx, 0x22a280654d8
    L00e1: mov rdx, [rdx]
    L00e4: mov rcx, rbx
    L00e7: call qword ptr [0x7fff0537e130]
    L00ed: mov rax, rbx
    L00f0: add rsp, 0x20
    L00f4: pop rbx
    L00f5: ret
    L00f6: mov rdx, 0x22a280654d0
    L0100: mov rdx, [rdx]
    L0103: mov rcx, rbx
    L0106: call qword ptr [0x7fff0537e130]
    L010c: jmp L008d
// .NET 8 (X64)
    L0000: push rdi
    L0001: push rsi
    L0002: push rbx
    L0003: sub rsp, 0x20
    L0007: mov rcx, 0x7fff054220e0
    L0011: call 0x00007fff64e3ae10
    L0016: mov rbx, rax
    L0019: mov rcx, 0x22c0c482818
    L0023: mov [rbx+8], rcx
    L0027: inc dword ptr [rbx+0x14]
    L002a: mov rcx, [rbx+8]
    L002e: cmp dword ptr [rcx+8], 3
    L0032: jl L00df
    L0038: cmp dword ptr [rbx+0x10], 3
    L003c: jg L0127
    L0042: mov dword ptr [rbx+0x10], 3
    L0049: mov rsi, [rbx+8]
    L004d: mov edi, [rbx+0x10]
    L0050: test rsi, rsi
    L0053: je L0120
    L0059: mov rcx, 0x7fff0535fb20
    L0063: cmp [rsi], rcx
    L0066: jne L0143
    L006c: cmp [rsi+8], edi
    L006f: jb L0120
    L0075: add rsi, 0x10
    L0079: test edi, edi
    L007b: je L014a
    L0081: mov rcx, 0x22a280654c8
    L008b: mov rdx, [rcx]
    L008e: mov rcx, rsi
    L0091: call 0x00007fff64e3a890
    L0096: cmp edi, 1
    L0099: jbe L014a
    L009f: lea rcx, [rsi+8]
    L00a3: mov rdx, 0x22a280654d0
    L00ad: mov rdx, [rdx]
    L00b0: call 0x00007fff64e3a890
    L00b5: cmp edi, 2
    L00b8: jbe L014a
    L00be: lea rcx, [rsi+0x10]
    L00c2: mov rdx, 0x22a280654d8
    L00cc: mov rdx, [rdx]
    L00cf: call 0x00007fff64e3a890
    L00d4: mov rax, rbx
    L00d7: add rsp, 0x20
    L00db: pop rbx
    L00dc: pop rsi
    L00dd: pop rdi
    L00de: ret
    L00df: mov rcx, [rbx+8]
    L00e3: cmp dword ptr [rcx+8], 0
    L00e7: jne short L0115
    L00e9: mov edx, 4
    L00ee: mov ecx, 0x7fffffc7
    L00f3: cmp edx, 0x7fffffc7
    L00f9: cmova edx, ecx
    L00fc: mov ecx, 3
    L0101: cmp edx, 3
    L0104: cmovl edx, ecx
    L0107: mov rcx, rbx
    L010a: call qword ptr [0x7fff0537dff8]
    L0110: jmp L0042
    L0115: mov rcx, [rbx+8]
    L0119: mov edx, [rcx+8]
    L011c: add edx, edx
    L011e: jmp short L00ee
    L0120: call qword ptr [0x7fff0565e478]
    L0126: int3
    L0127: mov r8d, [rbx+0x10]
    L012b: add r8d, 0xfffffffd
    L012f: mov rcx, [rbx+8]
    L0133: mov edx, 3
    L0138: call qword ptr [0x7fff052174c8]
    L013e: jmp L0042
    L0143: call qword ptr [0x7fff0565e430]
    L0149: int3
    L014a: call 0x00007fff64f60da0
    L014f: int3

Benchmark Description:

Collection expressions are new in C# 12 / .NET 8. The syntax is more succinct but they might be faster too going by the benchmarks above.

The provided benchmark code is designed to evaluate and compare the performance of two different ways to initialize and return a `List<string>` in C#. It is set up using BenchmarkDotNet, a powerful .NET library for benchmarking, which allows for precise and comprehensive performance measurements. The benchmark is configured to run on .NET 8, showcasing the use of the latest runtime features and improvements at the time of writing. ### General Setup - **.NET Version:** The benchmark specifies the use of .NET 8 (`CoreRuntime.Core80`), ensuring that the tests run on a specific, cutting-edge runtime environment. This choice is significant because performance characteristics can vary between runtime versions due to optimizations and new features. - **Configuration:** The benchmark uses a custom configuration class (`Config`) that extends `ManualConfig`. This configuration specifies the use of `.NET 8` and customizes the summary style to focus on trend ratios, which helps in understanding performance changes over time or between different methods. - **BenchmarkDotNet Attributes:** - `MemoryDiagnoser`: Activated to report memory allocation statistics, which are crucial for understanding the memory efficiency of the tested methods. - `ReturnValueValidator`: Ensures that the methods return the correct values, adding a layer of correctness verification to the performance tests. - `HideColumns`: Hides specific columns in the output report that are not essential for this benchmark, such as Job, Ratio Standard Deviation, and Allocation Ratio, for a cleaner and more focused presentation of results. ### Benchmark Methods #### 1. `RegularList()` - **Purpose:** Serves as the baseline for comparison. It initializes a `List<string>` in the traditional way, using the `new` keyword followed by collection initialization syntax. - **Performance Aspect:** Measures the time and memory allocations involved in creating and initializing a list using the conventional approach. This method is widely used and understood, making it a suitable baseline. - **Expected Insights:** As the baseline, the performance of this method will be the reference point. The insights here will be about how much more or less efficient alternative methods are compared to this standard approach. #### 2. `ColExpressionList()` - **Purpose:** Tests the performance of initializing a `List<string>` using a collection expression (assuming this is a new or hypothetical feature in C#, as it's not standard syntax as of .NET 6). - **Performance Aspect:** Evaluates whether using a collection expression (simplified syntax for collection initialization) offers any performance benefits or drawbacks compared to the traditional approach. This could include faster execution time, reduced memory allocations, or both. - **Expected Insights:** This method aims to reveal whether the newer or alternative syntax provides any significant performance improvements or penalties. If the collection expression syntax is more efficient, it could suggest a beneficial language feature for developers focusing on performance. Conversely, if it's less efficient or roughly the same, it might indicate that the traditional syntax is already optimized or that the new syntax offers other benefits (e.g., readability, maintainability) without compromising performance. ### Conclusion By running these benchmarks, developers can gain valuable insights into the performance implications of different collection initialization techniques in C#. This can guide best practices, especially in performance-critical applications, and contribute to a deeper understanding of how language features and syntax choices affect runtime efficiency.

Benchmark Comments: