Summing an integer array directly, with ArraySegment v using Span




Date Added (UTC):

15 Apr 2024 @ 00:24

Date Updated (UTC):

15 Apr 2024 @ 00:24


.NET Version(s):

.NET 8

Tag(s):

#Span<T>


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.Reports;
using System;

[Config(typeof(Config))]
[HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio)]
[MemoryDiagnoser]
[ReturnValueValidator(failOnError: true)]
public partial class SpanBenchmark
{
    private int[] data;

    [Params(1000)] 
    public int N;

    [GlobalSetup]
    public void Setup()
    {
        data = new int[N];
        var random = new Random(42);
        for (int i = 0; i < N; i++)
        {
            data[i] = random.Next(100);
        }
    }

    [Benchmark(Baseline = true)]
    public int ArrayDirect()
    {
        int sum = 0;
        for (int i = 0; i < data.Length; i++)
        {
            sum += data[i];
        }
        return sum;
    }

    [Benchmark]
    public int ArraySegmentManipulation()
    {
        int sum = 0;
        var segment = new ArraySegment<int>(data, 0, data.Length);
        for (int i = 0; i < segment.Count; i++)
        {
            sum += segment[i];
        }
        return sum;
    }

    [Benchmark]
    public int SpanManipulation()
    {
        int sum = 0;
        Span<int> span = data.AsSpan();
        for (int i = 0; i < span.Length; i++)
        {
            sum += span[i];
        }
        return sum;
    }
    private class Config : ManualConfig
    {
        public Config()
        {
            SummaryStyle =
                SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend);
        }
    }
}

// .NET 8
public int ArrayDirect()
{
    int num = 0;
    int num2 = 0;
    while (num2 < data.Length)
    {
        num += data[num2];
        num2++;
    }
    return num;
}
// .NET 8
public int ArraySegmentManipulation()
{
    int num = 0;
    ArraySegment<int> arraySegment = new ArraySegment<int>(data, 0, data.Length);
    int num2 = 0;
    while (num2 < arraySegment.Count)
    {
        num += arraySegment[num2];
        num2++;
    }
    return num;
}
// .NET 8
public int SpanManipulation()
{
    int num = 0;
    Span<int> span = MemoryExtensions.AsSpan(data);
    int num2 = 0;
    while (num2 < span.Length)
    {
        num += span[num2];
        num2++;
    }
    return num;
}

// .NET 8
.method public hidebysig 
    instance int32 ArrayDirect () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 28 00 00 00 01 5f 01 00 54 02 08 42 61 73
        65 6c 69 6e 65 01
    )
    // Method begins at RVA 0x2098
    // Code size 34 (0x22)
    .maxstack 3
    .locals init (
        [0] int32 sum,
        [1] int32 i
    )

    // sequence point: (line 43, col 9) to (line 43, col 21) in _
    IL_0000: ldc.i4.0
    IL_0001: stloc.0
    // sequence point: (line 44, col 14) to (line 44, col 23) in _
    IL_0002: ldc.i4.0
    IL_0003: stloc.1
    // sequence point: hidden
    IL_0004: br.s IL_0015
    // loop start (head: IL_0015)
        // sequence point: (line 46, col 13) to (line 46, col 28) in _
        IL_0006: ldloc.0
        IL_0007: ldarg.0
        IL_0008: ldfld int32[] SpanBenchmark::data
        IL_000d: ldloc.1
        IL_000e: ldelem.i4
        IL_000f: add
        IL_0010: stloc.0
        // sequence point: (line 44, col 42) to (line 44, col 45) in _
        IL_0011: ldloc.1
        IL_0012: ldc.i4.1
        IL_0013: add
        IL_0014: stloc.1

        // sequence point: (line 44, col 25) to (line 44, col 40) in _
        IL_0015: ldloc.1
        IL_0016: ldarg.0
        IL_0017: ldfld int32[] SpanBenchmark::data
        IL_001c: ldlen
        IL_001d: conv.i4
        IL_001e: blt.s IL_0006
    // end loop

    // sequence point: (line 48, col 9) to (line 48, col 20) in _
    IL_0020: ldloc.0
    IL_0021: ret
}
// .NET 8
.method public hidebysig 
    instance int32 ArraySegmentManipulation () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 33 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20c8
    // Code size 55 (0x37)
    .maxstack 4
    .locals init (
        [0] int32 sum,
        [1] valuetype [System.Runtime]System.ArraySegment`1<int32> segment,
        [2] int32 i
    )

    // sequence point: (line 54, col 9) to (line 54, col 21) in _
    IL_0000: ldc.i4.0
    IL_0001: stloc.0
    // sequence point: (line 55, col 9) to (line 55, col 67) in _
    IL_0002: ldloca.s 1
    IL_0004: ldarg.0
    IL_0005: ldfld int32[] SpanBenchmark::data
    IL_000a: ldc.i4.0
    IL_000b: ldarg.0
    IL_000c: ldfld int32[] SpanBenchmark::data
    IL_0011: ldlen
    IL_0012: conv.i4
    IL_0013: call instance void valuetype [System.Runtime]System.ArraySegment`1<int32>::.ctor(!0[], int32, int32)
    // sequence point: (line 56, col 14) to (line 56, col 23) in _
    IL_0018: ldc.i4.0
    IL_0019: stloc.2
    // sequence point: hidden
    IL_001a: br.s IL_002b
    // loop start (head: IL_002b)
        // sequence point: (line 58, col 13) to (line 58, col 31) in _
        IL_001c: ldloc.0
        IL_001d: ldloca.s 1
        IL_001f: ldloc.2
        IL_0020: call instance !0 valuetype [System.Runtime]System.ArraySegment`1<int32>::get_Item(int32)
        IL_0025: add
        IL_0026: stloc.0
        // sequence point: (line 56, col 44) to (line 56, col 47) in _
        IL_0027: ldloc.2
        IL_0028: ldc.i4.1
        IL_0029: add
        IL_002a: stloc.2

        // sequence point: (line 56, col 25) to (line 56, col 42) in _
        IL_002b: ldloc.2
        IL_002c: ldloca.s 1
        IL_002e: call instance int32 valuetype [System.Runtime]System.ArraySegment`1<int32>::get_Count()
        IL_0033: blt.s IL_001c
    // end loop

    // sequence point: (line 60, col 9) to (line 60, col 20) in _
    IL_0035: ldloc.0
    IL_0036: ret
}
// .NET 8
.method public hidebysig 
    instance int32 SpanManipulation () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 3f 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x210c
    // Code size 46 (0x2e)
    .maxstack 3
    .locals init (
        [0] int32 sum,
        [1] valuetype [System.Runtime]System.Span`1<int32> span,
        [2] int32 i
    )

    // sequence point: (line 66, col 9) to (line 66, col 21) in _
    IL_0000: ldc.i4.0
    IL_0001: stloc.0
    // sequence point: (line 67, col 9) to (line 67, col 40) in _
    IL_0002: ldarg.0
    IL_0003: ldfld int32[] SpanBenchmark::data
    IL_0008: call valuetype [System.Runtime]System.Span`1<!!0> [System.Memory]System.MemoryExtensions::AsSpan<int32>(!!0[])
    IL_000d: stloc.1
    // sequence point: (line 68, col 14) to (line 68, col 23) in _
    IL_000e: ldc.i4.0
    IL_000f: stloc.2
    // sequence point: hidden
    IL_0010: br.s IL_0022
    // loop start (head: IL_0022)
        // sequence point: (line 70, col 13) to (line 70, col 28) in _
        IL_0012: ldloc.0
        IL_0013: ldloca.s 1
        IL_0015: ldloc.2
        IL_0016: call instance !0& valuetype [System.Runtime]System.Span`1<int32>::get_Item(int32)
        IL_001b: ldind.i4
        IL_001c: add
        IL_001d: stloc.0
        // sequence point: (line 68, col 42) to (line 68, col 45) in _
        IL_001e: ldloc.2
        IL_001f: ldc.i4.1
        IL_0020: add
        IL_0021: stloc.2

        // sequence point: (line 68, col 25) to (line 68, col 40) in _
        IL_0022: ldloc.2
        IL_0023: ldloca.s 1
        IL_0025: call instance int32 valuetype [System.Runtime]System.Span`1<int32>::get_Length()
        IL_002a: blt.s IL_0012
    // end loop

    // sequence point: (line 72, col 9) to (line 72, col 20) in _
    IL_002c: ldloc.0
    IL_002d: ret
}

// .NET 8 (X64)
ArrayDirect()
    L0000: sub rsp, 0x28
    L0004: xor eax, eax
    L0006: xor edx, edx
    L0008: mov rcx, [rcx+8]
    L000c: cmp dword ptr [rcx+8], 0
    L0010: jle short L0038
    L0012: nop [rax]
    L0019: nop [rax]
    L0020: mov r8, rcx
    L0023: cmp edx, [r8+8]
    L0027: jae short L003d
    L0029: mov r10d, edx
    L002c: add eax, [r8+r10*4+0x10]
    L0031: inc edx
    L0033: cmp [rcx+8], edx
    L0036: jg short L0020
    L0038: add rsp, 0x28
    L003c: ret
    L003d: call 0x00007fff0a780da0
    L0042: int3
// .NET 8 (X64)
ArraySegmentManipulation()
    L0000: sub rsp, 0x28
    L0004: xor eax, eax
    L0006: mov rcx, [rcx+8]
    L000a: mov r8, rcx
    L000d: mov edx, [rcx+8]
    L0010: mov ecx, [r8+8]
    L0014: cmp ecx, edx
    L0016: jb short L0046
    L0018: xor r10d, r10d
    L001b: test edx, edx
    L001d: jle short L003a
    L001f: nop
    L0020: cmp r10d, edx
    L0023: jae short L003f
    L0025: mov r9d, r10d
    L0028: cmp r9d, ecx
    L002b: jae short L0055
    L002d: add eax, [r8+r9*4+0x10]
    L0032: inc r10d
    L0035: cmp r10d, edx
    L0038: jl short L0020
    L003a: add rsp, 0x28
    L003e: ret
    L003f: call qword ptr [0x7ffeaae4e508]
    L0045: int3
    L0046: mov rcx, r8
    L0049: mov r8d, edx
    L004c: xor edx, edx
    L004e: call qword ptr [0x7ffeaae4eb20]
    L0054: int3
    L0055: call 0x00007fff0a780da0
    L005a: int3
// .NET 8 (X64)
SpanManipulation()
    L0000: xor eax, eax
    L0002: mov rcx, [rcx+8]
    L0006: test rcx, rcx
    L0009: je short L0030
    L000b: lea rdx, [rcx+0x10]
    L000f: mov ecx, [rcx+8]
    L0012: xor r8d, r8d
    L0015: test ecx, ecx
    L0017: jle short L002f
    L0019: nop [rax]
    L0020: mov r10d, r8d
    L0023: add eax, [rdx+r10*4]
    L0027: inc r8d
    L002a: cmp r8d, ecx
    L002d: jl short L0020
    L002f: ret
    L0030: xor edx, edx
    L0032: xor ecx, ecx
    L0034: jmp short L0012


Benchmark Description:


The provided benchmark code is designed to measure and compare the performance of different methods for iterating over and summing the elements of an array in C#. It uses the BenchmarkDotNet library, a powerful tool for benchmarking .NET code. The benchmarks are configured to hide certain columns (Job, RatioSD, AllocRatio) for clarity and focus on memory usage through the `MemoryDiagnoser` attribute. The `ReturnValueValidator` ensures that benchmarks return the correct value, adding a layer of correctness verification to the performance tests. ### General Setup - **.NET Version**: Not explicitly mentioned, but the use of `Span<T>` indicates it targets .NET Core 2.1 or later, as `Span<T>` was introduced in .NET Core 2.1. - **Configuration**: Custom configuration is defined in the `Config` class, which modifies the summary style to emphasize the trend in the ratio of performance between benchmarks. - **Benchmark Setup**: The `GlobalSetup` method initializes an array of integers (`data`) with a fixed size (`N`, parameterized to 1000) and populates it with random numbers. This setup ensures that each benchmark method operates on the same dataset, allowing for fair comparison. ### Benchmark Methods #### 1. ArrayDirect - **Purpose**: This method serves as the baseline for comparison. It directly accesses elements in the array using a simple `for` loop and sums them. - **Performance Aspect**: It measures the raw performance of array access and iteration without any additional overhead or abstraction. - **Expected Insights**: Results from this benchmark provide a baseline for the most straightforward approach to summing array elements. Performance results here are expected to be the fastest or very close to the fastest, given the minimal overhead. #### 2. ArraySegmentManipulation - **Purpose**: To measure the performance impact of using `ArraySegment<T>` as a way to manipulate subsections of an array. - **Performance Aspect**: This benchmark assesses how wrapping an array in an `ArraySegment<T>` affects iteration and access performance, including any additional overhead introduced by this abstraction. - **Expected Insights**: Since `ArraySegment<T>` provides a layer of abstraction over the raw array, it might introduce slight overhead, resulting in slower performance compared to direct array access. However, it's important for understanding the trade-offs when using `ArraySegment<T>` for more flexible array manipulation. #### 3. SpanManipulation - **Purpose**: To evaluate the performance of using `Span<T>` for array manipulation, which is a newer type introduced for more efficient memory management in .NET Core. - **Performance Aspect**: This method tests the efficiency of `Span<T>` in terms of accessing and iterating over array elements, focusing on its performance relative to direct array access and `ArraySegment<T>`. - **Expected Insights**: `Span<T>` is designed for high-performance scenarios and provides a safe way to work with memory in .NET. The results from this benchmark are expected to be very competitive with direct array access, potentially outperforming `ArraySegment<T>` due to its design optimizations for such use cases. ### Conclusion Running these benchmarks will provide insights into the relative performance of different methods for summing elements in an array. It highlights the trade-offs between simplicity (direct array access), flexibility (`ArraySegment<T>`), and performance with safety (`Span<T>`). Understanding these results can guide developers in choosing the most appropriate method based on their specific requirements, whether they prioritize raw performance, memory safety, or the need to manipulate subsections of arrays.


Benchmark Comments: