1


Concat 50K one char strings with + and StringBuilder in .NET 8




Date Added (UTC):

10 Apr 2024 @ 23:38

Date Updated (UTC):

10 Apr 2024 @ 23:39


.NET Version(s):

.NET 8

Tag(s):

#StringBuilder #StringConcatenation #Strings


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)]
    [MemoryDiagnoser]
    [HideColumns(Column.RatioSD, Column.AllocRatio, Column.Error, Column.StdDev)]
    public class StringConcatInLoop
    {
        [Params(50, 50000)]
        public int Total { get; set; }

        [Benchmark(Baseline = true)]
        public string Plus()
        {
            var result = string.Empty;
            for (int i = 0; i < Total; i++)
            {
                result = result + '*';
            }

            return result;
        }

        [Benchmark]
        public string SB()
        {
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < Total; i++)
            {
                result.Append('*');
            }

            return result.ToString();
        }

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

// .NET 8
public string Plus()
{
    string text = string.Empty;
    int num = 0;
    while (num < Total)
    {
        text = string.Concat(text, "*");
        num++;
    }
    return text;
}
// .NET 8
public string SB()
{
    StringBuilder stringBuilder = new StringBuilder();
    int num = 0;
    while (num < Total)
    {
        stringBuilder.Append('*');
        num++;
    }
    return stringBuilder.ToString();
}

// .NET 8
.method public hidebysig 
    instance string Plus () 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 01 00 54 02 08 42 61 73
        65 6c 69 6e 65 01
    )
    // Method begins at RVA 0x2064
    // Code size 37 (0x25)
    .maxstack 2
    .locals init (
        [0] string result,
        [1] int32 i
    )

    // sequence point: (line 33, col 13) to (line 33, col 39) in _
    IL_0000: ldsfld string [System.Runtime]System.String::Empty
    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 39) in _
        IL_000a: ldloc.0
        IL_000b: ldstr "*"
        IL_0010: call string [System.Runtime]System.String::Concat(string, string)
        IL_0015: stloc.0
        // sequence point: (line 34, col 40) to (line 34, col 43) 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 38) in _
        IL_001a: ldloc.1
        IL_001b: ldarg.0
        IL_001c: call instance int32 BenchmarkDotNet.Samples.StringConcatInLoop::get_Total()
        IL_0021: blt.s IL_000a
    // end loop

    // sequence point: (line 39, col 13) to (line 39, col 27) in _
    IL_0023: ldloc.0
    IL_0024: ret
}
// .NET 8
.method public hidebysig 
    instance string SB () 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 0x2098
    // Code size 39 (0x27)
    .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_0017
    // loop start (head: IL_0017)
        // sequence point: (line 48, col 17) to (line 48, col 36) in _
        IL_000a: ldloc.0
        IL_000b: ldc.i4.s 42
        IL_000d: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
        IL_0012: pop
        // sequence point: (line 46, col 40) to (line 46, col 43) in _
        IL_0013: ldloc.1
        IL_0014: ldc.i4.1
        IL_0015: add
        IL_0016: stloc.1

        // sequence point: (line 46, col 29) to (line 46, col 38) in _
        IL_0017: ldloc.1
        IL_0018: ldarg.0
        IL_0019: call instance int32 BenchmarkDotNet.Samples.StringConcatInLoop::get_Total()
        IL_001e: blt.s IL_000a
    // end loop

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

// .NET 8 (X64)
Plus()
    L0000: push rdi
    L0001: push rsi
    L0002: push rbx
    L0003: sub rsp, 0x20
    L0007: mov rbx, rcx
    L000a: mov rax, 0x1815e290008
    L0014: xor esi, esi
    L0016: cmp dword ptr [rbx+8], 0
    L001a: jle short L003c
    L001c: mov rdx, 0x17f7905fa58
    L0026: mov rdi, [rdx]
    L0029: mov rdx, rdi
    L002c: mov rcx, rax
    L002f: call qword ptr [0x7ffcda776b08]
    L0035: inc esi
    L0037: cmp esi, [rbx+8]
    L003a: jl short L0029
    L003c: add rsp, 0x20
    L0040: pop rbx
    L0041: pop rsi
    L0042: pop rdi
    L0043: ret
// .NET 8 (X64)
SB()
    L0000: push rdi
    L0001: push rsi
    L0002: push rbx
    L0003: sub rsp, 0x20
    L0007: mov rbx, rcx
    L000a: mov rcx, 0x7ffcda8f29c0
    L0014: call 0x00007ffd3a2fae10
    L0019: mov rsi, rax
    L001c: mov dword ptr [rsi+0x20], 0x7fffffff
    L0023: mov rcx, 0x7ffcda8f3038
    L002d: mov edx, 0x10
    L0032: call 0x00007ffd3a2faf30
    L0037: lea rcx, [rsi+8]
    L003b: mov rdx, rax
    L003e: call 0x00007ffd3a2fa680
    L0043: xor edi, edi
    L0045: cmp dword ptr [rbx+8], 0
    L0049: jle short L006e
    L004b: mov ecx, [rsi+0x18]
    L004e: mov edx, ecx
    L0050: mov rax, [rsi+8]
    L0054: cmp [rax+8], edx
    L0057: jbe short L007e
    L0059: mov edx, edx
    L005b: mov word ptr [rax+rdx*2+0x10], 0x2a
    L0062: inc ecx
    L0064: mov [rsi+0x18], ecx
    L0067: inc edi
    L0069: cmp edi, [rbx+8]
    L006c: jl short L004b
    L006e: mov rcx, rsi
    L0071: add rsp, 0x20
    L0075: pop rbx
    L0076: pop rsi
    L0077: pop rdi
    L0078: jmp qword ptr [0x7ffcda8f2a28]
    L007e: mov rcx, rsi
    L0081: mov edx, 0x2a
    L0086: call qword ptr [0x7ffcda8ed2f0]
    L008c: jmp short L0067


Benchmark Description:


Typical benchmark comparing string + versus StringBuilder to show how poorly + performs in terms of both speed and memory when we are concatenating in large loops. ***Note***, in this case since it's a benchmark we know how many iterations there will be. In a real app if we have a collection of known values and we want to concatenate them, other approaches such as String.Join or String.Concat will likely beat StringBuilder. SB excels when we do not know how many concatenations there will be.

### General Setup Overview The provided benchmark code is designed to run within the BenchmarkDotNet framework, a powerful library for benchmarking .NET applications. The configuration and setup for this benchmark include several key components: - **.NET Version:** The benchmark specifies the use of `.NET 8.0` through the `[SimpleJob(RuntimeMoniker.Net80)]` attribute. This ensures that the benchmark runs on a specific version of the .NET runtime, allowing for consistent and targeted performance testing. - **Configuration Class (`Config`):** A custom configuration class is defined and applied to the benchmark class via the `[Config(typeof(Config))]` attribute. This configuration modifies the default behavior and output of BenchmarkDotNet, including: - **Summary Style:** Customizes the summary report to display results in milliseconds and to show ratio comparisons as percentages. - **Hidden Columns:** Certain columns like Ratio Standard Deviation, Allocation Ratio, Error, and Standard Deviation are hidden to simplify the output. - **Memory Diagnoser:** The `[MemoryDiagnoser]` attribute is used to enable memory diagnostics, allowing the benchmark to report on memory allocations, which is crucial for understanding the memory efficiency of different methods. - **Parameters (`Total`):** The benchmark includes a parameter `Total` with two values, `50` and `50000`, to test the methods under different load conditions, representing small and large string concatenations respectively. ### Benchmark Methods Rationale #### 1. `Plus()` Method - **Purpose:** This method tests the performance of string concatenation using the `+` operator inside a loop. It's a common pattern in less performance-critical code or among developers who are not aware of the potential performance implications. - **Performance Aspect:** It specifically measures the time and memory overhead associated with repeated string concatenations using the `+` operator. Each concatenation creates a new string object, leading to significant memory allocations and potentially slower performance, especially with a large number of concatenations. - **Expected Insights:** For small values of `Total`, this method might perform reasonably well, but as `Total` increases, we expect to see a substantial increase in both execution time and memory allocations. This method serves as a baseline to highlight the inefficiency of using `+` for multiple string concatenations in a loop. #### 2. `SB()` Method (StringBuilder) - **Purpose:** This method evaluates the performance of string concatenation using the `StringBuilder` class, which is designed to efficiently handle multiple string concatenations by modifying an existing buffer rather than creating new string instances. - **Performance Aspect:** It aims to measure how much faster and memory-efficient `StringBuilder` is compared to the basic `+` operator for string concatenation, especially when dealing with a large number of concatenations. - **Expected Insights:** The `StringBuilder` method is expected to show significantly better performance and lower memory usage than the `Plus()` method, particularly as the value of `Total` increases. This method demonstrates the importance of using `StringBuilder` for scenarios involving numerous string concatenations. ### Conclusion By comparing the performance of these two methods under different conditions (`Total` = 50 and 50000), users can gain clear insights into the importance of choosing the right approach for string concatenation in .NET applications. The benchmark is designed to highlight the efficiency of `StringBuilder` over the naive `+` operator approach, providing a compelling case for using `StringBuilder` in performance-critical code paths.


Benchmark Comments: