StringBuilder Append performance improvement for constants in .NET 8 (versus .NET 7)




Date Added (UTC):

05 Apr 2024 @ 04:59

Date Updated (UTC):

05 Apr 2024 @ 05:02


.NET Version(s):

.NET 7 .NET 8

Tag(s):

#.Net8PerfImprovement #StringBuilder #StringConcatenation


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;

namespace BenchmarkDotNet.Samples
{
    [Config(typeof(Config))]
    [SimpleJob(RuntimeMoniker.Net80)]
    [SimpleJob(RuntimeMoniker.Net70, baseline: true)]
    [HideColumns(Column.RatioSD, Column.AllocRatio, Column.Error, Column.StdDev, Column.Runtime)]
    public class StringBuilderAppend
    {
        [Params(100)]
        public int Concats { get; set; }

        [Benchmark]
        public string SB_10()
        {
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < Concats; i++)
            {
                result.Append("1234567890");
            }

            return result.ToString();
        }

        [Benchmark]
        public string SB_100()
        {
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < Concats; i++)
            {
                result.Append("khtFRrQOx0lsxNOSJuoyon2NE2csvMRkhZ9OqnrLQe63UIPlMu3Eyn2iWZh7UDfT8wrK5t2K8vOO2JUk4ymi9l0K4zNXD5KqkzVy");
            }

            return result.ToString();
        }

        private class Config : ManualConfig
        {
            public Config()
            {
                SummaryStyle =
                    SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage)
                    .WithTimeUnit(Perfolizer.Horology.TimeUnit.Millisecond);
            }
        }
    }
}

// .NET 7, .NET 8
public string SB_10()
{
    StringBuilder stringBuilder = new StringBuilder();
    int num = 0;
    while (num < Concats)
    {
        stringBuilder.Append("1234567890");
        num++;
    }
    return stringBuilder.ToString();
}
// .NET 7, .NET 8
public string SB_100()
{
    StringBuilder stringBuilder = new StringBuilder();
    int num = 0;
    while (num < Concats)
    {
        stringBuilder.Append("khtFRrQOx0lsxNOSJuoyon2NE2csvMRkhZ9OqnrLQe63UIPlMu3Eyn2iWZh7UDfT8wrK5t2K8vOO2JUk4ymi9l0K4zNXD5KqkzVy");
        num++;
    }
    return stringBuilder.ToString();
}

// .NET 7
.method public hidebysig 
    instance string SB_10 () 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 1e 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20a0
    // Code size 42 (0x2a)
    .maxstack 2
    .locals init (
        [0] class [System.Runtime]System.Text.StringBuilder result,
        [1] int32 i
    )

    // sequence point: (line 33, col 13) to (line 33, col 56) in _
    IL_0000: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor()
    IL_0005: stloc.0
    // sequence point: (line 34, col 18) to (line 34, col 27) in _
    IL_0006: ldc.i4.0
    IL_0007: stloc.1
    // sequence point: hidden
    IL_0008: br.s IL_001a
    // loop start (head: IL_001a)
        // sequence point: (line 36, col 17) to (line 36, col 45) in _
        IL_000a: ldloc.0
        IL_000b: ldstr "1234567890"
        IL_0010: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
        IL_0015: pop
        // sequence point: (line 34, col 42) to (line 34, col 45) in _
        IL_0016: ldloc.1
        IL_0017: ldc.i4.1
        IL_0018: add
        IL_0019: stloc.1

        // sequence point: (line 34, col 29) to (line 34, col 40) in _
        IL_001a: ldloc.1
        IL_001b: ldarg.0
        IL_001c: call instance int32 BenchmarkDotNet.Samples.StringBuilderAppend::get_Concats()
        IL_0021: blt.s IL_000a
    // end loop

    // sequence point: (line 39, col 13) to (line 39, col 38) in _
    IL_0023: ldloc.0
    IL_0024: callvirt instance string [System.Runtime]System.Object::ToString()
    IL_0029: ret
}

// .NET 8
.method public hidebysig 
    instance string SB_10 () 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 1e 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2064
    // Code size 42 (0x2a)
    .maxstack 2
    .locals init (
        [0] class [System.Runtime]System.Text.StringBuilder result,
        [1] int32 i
    )

    // sequence point: (line 33, col 13) to (line 33, col 56) in _
    IL_0000: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor()
    IL_0005: stloc.0
    // sequence point: (line 34, col 18) to (line 34, col 27) in _
    IL_0006: ldc.i4.0
    IL_0007: stloc.1
    // sequence point: hidden
    IL_0008: br.s IL_001a
    // loop start (head: IL_001a)
        // sequence point: (line 36, col 17) to (line 36, col 45) in _
        IL_000a: ldloc.0
        IL_000b: ldstr "1234567890"
        IL_0010: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
        IL_0015: pop
        // sequence point: (line 34, col 42) to (line 34, col 45) in _
        IL_0016: ldloc.1
        IL_0017: ldc.i4.1
        IL_0018: add
        IL_0019: stloc.1

        // sequence point: (line 34, col 29) to (line 34, col 40) in _
        IL_001a: ldloc.1
        IL_001b: ldarg.0
        IL_001c: call instance int32 BenchmarkDotNet.Samples.StringBuilderAppend::get_Concats()
        IL_0021: blt.s IL_000a
    // end loop

    // sequence point: (line 39, col 13) to (line 39, col 38) in _
    IL_0023: ldloc.0
    IL_0024: callvirt instance string [System.Runtime]System.Object::ToString()
    IL_0029: ret
}
// .NET 7
.method public hidebysig 
    instance string SB_100 () 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 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20d8
    // Code size 42 (0x2a)
    .maxstack 2
    .locals init (
        [0] class [System.Runtime]System.Text.StringBuilder result,
        [1] int32 i
    )

    // sequence point: (line 45, col 13) to (line 45, col 56) in _
    IL_0000: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor()
    IL_0005: stloc.0
    // sequence point: (line 46, col 18) to (line 46, col 27) in _
    IL_0006: ldc.i4.0
    IL_0007: stloc.1
    // sequence point: hidden
    IL_0008: br.s IL_001a
    // loop start (head: IL_001a)
        // sequence point: (line 48, col 17) to (line 48, col 135) in _
        IL_000a: ldloc.0
        IL_000b: ldstr "khtFRrQOx0lsxNOSJuoyon2NE2csvMRkhZ9OqnrLQe63UIPlMu3Eyn2iWZh7UDfT8wrK5t2K8vOO2JUk4ymi9l0K4zNXD5KqkzVy"
        IL_0010: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
        IL_0015: pop
        // sequence point: (line 46, col 42) to (line 46, col 45) in _
        IL_0016: ldloc.1
        IL_0017: ldc.i4.1
        IL_0018: add
        IL_0019: stloc.1

        // sequence point: (line 46, col 29) to (line 46, col 40) in _
        IL_001a: ldloc.1
        IL_001b: ldarg.0
        IL_001c: call instance int32 BenchmarkDotNet.Samples.StringBuilderAppend::get_Concats()
        IL_0021: blt.s IL_000a
    // end loop

    // sequence point: (line 51, col 13) to (line 51, col 38) in _
    IL_0023: ldloc.0
    IL_0024: callvirt instance string [System.Runtime]System.Object::ToString()
    IL_0029: ret
}

// .NET 8
.method public hidebysig 
    instance string SB_100 () 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 2a 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x209c
    // Code size 42 (0x2a)
    .maxstack 2
    .locals init (
        [0] class [System.Runtime]System.Text.StringBuilder result,
        [1] int32 i
    )

    // sequence point: (line 45, col 13) to (line 45, col 56) in _
    IL_0000: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor()
    IL_0005: stloc.0
    // sequence point: (line 46, col 18) to (line 46, col 27) in _
    IL_0006: ldc.i4.0
    IL_0007: stloc.1
    // sequence point: hidden
    IL_0008: br.s IL_001a
    // loop start (head: IL_001a)
        // sequence point: (line 48, col 17) to (line 48, col 135) in _
        IL_000a: ldloc.0
        IL_000b: ldstr "khtFRrQOx0lsxNOSJuoyon2NE2csvMRkhZ9OqnrLQe63UIPlMu3Eyn2iWZh7UDfT8wrK5t2K8vOO2JUk4ymi9l0K4zNXD5KqkzVy"
        IL_0010: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
        IL_0015: pop
        // sequence point: (line 46, col 42) to (line 46, col 45) in _
        IL_0016: ldloc.1
        IL_0017: ldc.i4.1
        IL_0018: add
        IL_0019: stloc.1

        // sequence point: (line 46, col 29) to (line 46, col 40) in _
        IL_001a: ldloc.1
        IL_001b: ldarg.0
        IL_001c: call instance int32 BenchmarkDotNet.Samples.StringBuilderAppend::get_Concats()
        IL_0021: blt.s IL_000a
    // end loop

    // sequence point: (line 51, col 13) to (line 51, col 38) in _
    IL_0023: ldloc.0
    IL_0024: callvirt instance string [System.Runtime]System.Object::ToString()
    IL_0029: ret
}

// .NET 7 (X64)
SB_10()
    L0000: push rdi
    L0001: push rsi
    L0002: push rbp
    L0003: push rbx
    L0004: sub rsp, 0x28
    L0008: mov rsi, rcx
    L000b: mov rcx, 0x7ff8a14c9e80
    L0015: call 0x00007ff900e5f550
    L001a: mov rdi, rax
    L001d: mov dword ptr [rdi+0x20], 0x7fffffff
    L0024: mov rcx, 0x7ff8a14cd2f8
    L002e: mov edx, 0x10
    L0033: call 0x00007ff900e5f670
    L0038: lea rcx, [rdi+8]
    L003c: mov rdx, rax
    L003f: call 0x00007ff900e5edc0
    L0044: xor ebx, ebx
    L0046: cmp dword ptr [rsi+8], 0
    L004a: jle short L0076
    L004c: mov rdx, 0x1e2cb490338
    L0056: mov rbp, [rdx]
    L0059: add rbp, 0xc
    L005d: mov rdx, rbp
    L0060: mov rcx, rdi
    L0063: mov r8d, 0xa
    L0069: call qword ptr [0x7ff8a14d39c0]
    L006f: inc ebx
    L0071: cmp ebx, [rsi+8]
    L0074: jl short L005d
    L0076: mov rcx, rdi
    L0079: add rsp, 0x28
    L007d: pop rbx
    L007e: pop rbp
    L007f: pop rsi
    L0080: pop rdi
    L0081: jmp qword ptr [0x7ff8a14c9ee8]
// .NET 8 (X64)
SB_10()
    L0000: push rdi
    L0001: push rsi
    L0002: push rbp
    L0003: push rbx
    L0004: sub rsp, 0x28
    L0008: vzeroupper
    L000b: mov rbx, rcx
    L000e: mov rcx, 0x7ffbe65229c0
    L0018: call 0x00007ffc45f0ae10
    L001d: mov rsi, rax
    L0020: mov dword ptr [rsi+0x20], 0x7fffffff
    L0027: mov rcx, 0x7ffbe6523038
    L0031: mov edx, 0x10
    L0036: call 0x00007ffc45f0af30
    L003b: lea rcx, [rsi+8]
    L003f: mov rdx, rax
    L0042: call 0x00007ffc45f0a680
    L0047: xor edi, edi
    L0049: cmp dword ptr [rbx+8], 0
    L004d: jle short L009d
    L004f: mov rcx, 0x150a80654d0
    L0059: mov rbp, [rcx]
    L005c: add rbp, 0xc
    L0060: mov rdx, rbp
    L0063: mov rcx, [rsi+8]
    L0067: mov r8d, [rsi+0x18]
    L006b: lea eax, [r8+0xa]
    L006f: cmp [rcx+8], eax
    L0072: jb short L00ae
    L0074: movsxd rax, r8d
    L0077: lea rcx, [rcx+rax*2+0x10]
    L007c: vmovdqu xmm0, [rdx]
    L0080: vmovdqu xmm1, [rdx+4]
    L0085: vmovdqu [rcx], xmm0
    L0089: vmovdqu [rcx+4], xmm1
    L008e: add r8d, 0xa
    L0092: mov [rsi+0x18], r8d
    L0096: inc edi
    L0098: cmp edi, [rbx+8]
    L009b: jl short L0060
    L009d: mov rcx, rsi
    L00a0: add rsp, 0x28
    L00a4: pop rbx
    L00a5: pop rbp
    L00a6: pop rsi
    L00a7: pop rdi
    L00a8: jmp qword ptr [0x7ffbe6522a28]
    L00ae: mov rcx, rsi
    L00b1: mov r8d, 0xa
    L00b7: call qword ptr [0x7ffbe651d998]
    L00bd: jmp short L0096
// .NET 7 (X64)
SB_100()
    L0000: push rdi
    L0001: push rsi
    L0002: push rbp
    L0003: push rbx
    L0004: sub rsp, 0x28
    L0008: mov rsi, rcx
    L000b: mov rcx, 0x7ff8a14c9e80
    L0015: call 0x00007ff900e5f550
    L001a: mov rdi, rax
    L001d: mov dword ptr [rdi+0x20], 0x7fffffff
    L0024: mov rcx, 0x7ff8a14cd2f8
    L002e: mov edx, 0x10
    L0033: call 0x00007ff900e5f670
    L0038: lea rcx, [rdi+8]
    L003c: mov rdx, rax
    L003f: call 0x00007ff900e5edc0
    L0044: xor ebx, ebx
    L0046: cmp dword ptr [rsi+8], 0
    L004a: jle short L0076
    L004c: mov rdx, 0x1e2cb490340
    L0056: mov rbp, [rdx]
    L0059: add rbp, 0xc
    L005d: mov rdx, rbp
    L0060: mov rcx, rdi
    L0063: mov r8d, 0x64
    L0069: call qword ptr [0x7ff8a14d39c0]
    L006f: inc ebx
    L0071: cmp ebx, [rsi+8]
    L0074: jl short L005d
    L0076: mov rcx, rdi
    L0079: add rsp, 0x28
    L007d: pop rbx
    L007e: pop rbp
    L007f: pop rsi
    L0080: pop rdi
    L0081: jmp qword ptr [0x7ff8a14c9ee8]
// .NET 8 (X64)
SB_100()
    L0000: push r14
    L0002: push rdi
    L0003: push rsi
    L0004: push rbp
    L0005: push rbx
    L0006: sub rsp, 0x20
    L000a: mov rbx, rcx
    L000d: mov rcx, 0x7ffbe65229c0
    L0017: call 0x00007ffc45f0ae10
    L001c: mov rsi, rax
    L001f: mov dword ptr [rsi+0x20], 0x7fffffff
    L0026: mov rcx, 0x7ffbe6523038
    L0030: mov edx, 0x10
    L0035: call 0x00007ffc45f0af30
    L003a: lea rcx, [rsi+8]
    L003e: mov rdx, rax
    L0041: call 0x00007ffc45f0a680
    L0046: xor edi, edi
    L0048: cmp dword ptr [rbx+8], 0
    L004c: jle short L0097
    L004e: mov rcx, 0x150a80654d8
    L0058: mov rbp, [rcx]
    L005b: add rbp, 0xc
    L005f: mov rdx, rbp
    L0062: mov rcx, [rsi+8]
    L0066: mov r14d, [rsi+0x18]
    L006a: lea r8d, [r14+0x64]
    L006e: cmp [rcx+8], r8d
    L0072: jb short L00aa
    L0074: movsxd r8, r14d
    L0077: lea rcx, [rcx+r8*2+0x10]
    L007c: mov r8d, 0xc8
    L0082: call qword ptr [0x7ffbe6445b78]
    L0088: add r14d, 0x64
    L008c: mov [rsi+0x18], r14d
    L0090: inc edi
    L0092: cmp edi, [rbx+8]
    L0095: jl short L005f
    L0097: mov rcx, rsi
    L009a: add rsp, 0x20
    L009e: pop rbx
    L009f: pop rbp
    L00a0: pop rsi
    L00a1: pop rdi
    L00a2: pop r14
    L00a4: jmp qword ptr [0x7ffbe6522a28]
    L00aa: mov rcx, rsi
    L00ad: mov r8d, 0x64
    L00b3: call qword ptr [0x7ffbe651d998]
    L00b9: jmp short L0090


Benchmark Description:


In .NET 8 the JIT can unroll the memory copies that occur as part of appending a constant string. [https://github.com/dotnet/runtime/pull/85894](https://github.com/dotnet/runtime/pull/85894)

The provided benchmark code is designed to measure and compare the performance of the `StringBuilder` class in .NET when appending strings of different lengths. The benchmarks are set up to run on two different .NET runtime versions, .NET 8.0 and .NET 7.0, with .NET 7.0 being the baseline for comparison. This setup allows for the evaluation of any performance improvements or regressions between these two runtime versions. ### General Setup - **.NET Versions**: The benchmarks are configured to run on .NET 8.0 and .NET 7.0, with .NET 7.0 as the baseline. This means the performance results from .NET 8.0 will be compared against those from .NET 7.0 to determine any improvements or regressions. - **Configuration**: The `Config` class customizes the output of the benchmark results. It sets the summary style to display ratios as percentages and the time unit for benchmark results to milliseconds. This makes the results easier to read and understand, especially when comparing the performance between different runtime versions or methods. - **Hidden Columns**: Certain columns like RatioSD, AllocRatio, Error, StdDev, and Runtime are hidden to simplify the output and focus on the most relevant metrics for this benchmark. ### Benchmark Methods #### SB_10 Method - **Purpose**: This method is designed to test the performance of appending a short string ("1234567890") to a `StringBuilder` instance repeatedly (`Concats` times, where `Concats` is set to 100). - **Performance Aspect**: It measures how efficiently `StringBuilder` can handle small, repeated append operations. This is a common scenario in many applications where concatenating small strings is necessary. - **Expected Insights**: The results will indicate how well the `StringBuilder` performs under the pressure of numerous small append operations. A more efficient implementation in one of the .NET versions will result in shorter execution times. This benchmark is important because it simulates a frequent use case in software development, highlighting potential performance bottlenecks or improvements in string handling. #### SB_100 Method - **Purpose**: This method evaluates the performance of appending a long string (100 characters) to a `StringBuilder` instance repeatedly (`Concats` times). - **Performance Aspect**: It focuses on `StringBuilder`'s capability to handle larger append operations efficiently. This scenario is relevant for applications that need to construct large strings dynamically. - **Expected Insights**: By comparing the execution time of this method across different .NET versions, one can assess any changes in the performance of `StringBuilder` when dealing with large strings. An efficient handling of such scenarios is crucial for applications that perform extensive string manipulation, as it can significantly impact overall performance. ### Conclusion Running these benchmarks will provide valuable insights into the performance characteristics of the `StringBuilder` class across different .NET versions and for different use cases (appending short vs. long strings). Understanding these characteristics is essential for optimizing string manipulation in .NET applications, ensuring they run as efficiently as possible.


Benchmark Comments: