StringBuilder with and without initial capacity - .NET 8




Date Added (UTC):

05 Apr 2024 @ 04:34

Date Updated (UTC):

05 Apr 2024 @ 04:34


.NET Version(s):

.NET 8

Tag(s):

#StringBuilder #StringConcatenation


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

namespace Benchmarks
{
    [MemoryDiagnoser]
    [Config(typeof(Config))]
    [SimpleJob(RuntimeMoniker.Net80)]
    [HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio)]
    public class StringBuilderInitialCapacity
    {
        private class Config : ManualConfig
        {
            public Config()
            {
                SummaryStyle =
                    SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
            }
        }

        private string
            title = "Mr.", firstName = "David", middleName = "Patrick", lastName = "Callan";

        [Benchmark(Baseline = true)]
        public string StringBuilder()
        {
            var stringBuilder =
                new StringBuilder();

            return stringBuilder
                .Append(title).Append(' ')
                .Append(firstName).Append(' ')
                .Append(middleName).Append(' ')
                .Append(lastName).ToString();
        }

        [Benchmark]
        public string StringBuilderExact24()
        {
            var stringBuilder =
                new StringBuilder(24);

            return stringBuilder
                .Append(title).Append(' ')
                .Append(firstName).Append(' ')
                .Append(middleName).Append(' ')
                .Append(lastName).ToString();
        }

        [Benchmark]
        public string StringBuilderEstimate100()
        {
            var stringBuilder =
                new StringBuilder(100);

            return stringBuilder
                .Append(title).Append(' ')
                .Append(firstName).Append(' ')
                .Append(middleName).Append(' ')
                .Append(lastName).ToString();
        }
    }
}

// .NET 8
public string StringBuilder()
{
    return new StringBuilder().Append(title).Append(' ').Append(firstName)
        .Append(' ')
        .Append(middleName)
        .Append(' ')
        .Append(lastName)
        .ToString();
}
// .NET 8
public string StringBuilderExact24()
{
    return new StringBuilder(24).Append(title).Append(' ').Append(firstName)
        .Append(' ')
        .Append(middleName)
        .Append(' ')
        .Append(lastName)
        .ToString();
}
// .NET 8
public string StringBuilderEstimate100()
{
    return new StringBuilder(100).Append(title).Append(' ').Append(firstName)
        .Append(' ')
        .Append(middleName)
        .Append(' ')
        .Append(lastName)
        .ToString();
}

// .NET 8
.method public hidebysig 
    instance string StringBuilder () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 27 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 76 (0x4c)
    .maxstack 2

    // sequence point: (line 42, col 13) to (line 43, col 37) in _
    IL_0000: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor()
    // sequence point: (line 45, col 13) to (line 49, col 46) in _
    IL_0005: ldarg.0
    IL_0006: ldfld string Benchmarks.StringBuilderInitialCapacity::title
    IL_000b: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0010: ldc.i4.s 32
    IL_0012: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_0017: ldarg.0
    IL_0018: ldfld string Benchmarks.StringBuilderInitialCapacity::firstName
    IL_001d: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0022: ldc.i4.s 32
    IL_0024: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_0029: ldarg.0
    IL_002a: ldfld string Benchmarks.StringBuilderInitialCapacity::middleName
    IL_002f: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0034: ldc.i4.s 32
    IL_0036: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_003b: ldarg.0
    IL_003c: ldfld string Benchmarks.StringBuilderInitialCapacity::lastName
    IL_0041: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0046: callvirt instance string [System.Runtime]System.Object::ToString()
    IL_004b: ret
}
// .NET 8
.method public hidebysig 
    instance string StringBuilderExact24 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 34 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20a8
    // Code size 78 (0x4e)
    .maxstack 2

    // sequence point: (line 55, col 13) to (line 56, col 39) in _
    IL_0000: ldc.i4.s 24
    IL_0002: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor(int32)
    // sequence point: (line 58, col 13) to (line 62, col 46) in _
    IL_0007: ldarg.0
    IL_0008: ldfld string Benchmarks.StringBuilderInitialCapacity::title
    IL_000d: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0012: ldc.i4.s 32
    IL_0014: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_0019: ldarg.0
    IL_001a: ldfld string Benchmarks.StringBuilderInitialCapacity::firstName
    IL_001f: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0024: ldc.i4.s 32
    IL_0026: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_002b: ldarg.0
    IL_002c: ldfld string Benchmarks.StringBuilderInitialCapacity::middleName
    IL_0031: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0036: ldc.i4.s 32
    IL_0038: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_003d: ldarg.0
    IL_003e: ldfld string Benchmarks.StringBuilderInitialCapacity::lastName
    IL_0043: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0048: callvirt instance string [System.Runtime]System.Object::ToString()
    IL_004d: ret
}
// .NET 8
.method public hidebysig 
    instance string StringBuilderEstimate100 () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 41 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2104
    // Code size 78 (0x4e)
    .maxstack 2

    // sequence point: (line 68, col 13) to (line 69, col 40) in _
    IL_0000: ldc.i4.s 100
    IL_0002: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor(int32)
    // sequence point: (line 71, col 13) to (line 75, col 46) in _
    IL_0007: ldarg.0
    IL_0008: ldfld string Benchmarks.StringBuilderInitialCapacity::title
    IL_000d: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0012: ldc.i4.s 32
    IL_0014: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_0019: ldarg.0
    IL_001a: ldfld string Benchmarks.StringBuilderInitialCapacity::firstName
    IL_001f: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0024: ldc.i4.s 32
    IL_0026: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_002b: ldarg.0
    IL_002c: ldfld string Benchmarks.StringBuilderInitialCapacity::middleName
    IL_0031: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0036: ldc.i4.s 32
    IL_0038: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
    IL_003d: ldarg.0
    IL_003e: ldfld string Benchmarks.StringBuilderInitialCapacity::lastName
    IL_0043: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
    IL_0048: callvirt instance string [System.Runtime]System.Object::ToString()
    IL_004d: ret
}

// .NET 8 (X64)
StringBuilder()
    L0000: push rsi
    L0001: push rbx
    L0002: sub rsp, 0x28
    L0006: mov rbx, rcx
    L0009: mov rcx, 0x7ff93e6e29c0
    L0013: call 0x00007ff99e0fae10
    L0018: mov rsi, rax
    L001b: mov dword ptr [rsi+0x20], 0x7fffffff
    L0022: mov rcx, 0x7ff93e6e3038
    L002c: mov edx, 0x10
    L0031: call 0x00007ff99e0faf30
    L0036: lea rcx, [rsi+8]
    L003a: mov rdx, rax
    L003d: call 0x00007ff99e0fa680
    L0042: mov rdx, [rbx+8]
    L0046: test rdx, rdx
    L0049: je short L005c
    L004b: mov r8d, [rdx+8]
    L004f: add rdx, 0xc
    L0053: mov rcx, rsi
    L0056: call qword ptr [0x7ff93e6dd980]
    L005c: mov ecx, [rsi+0x18]
    L005f: mov rdx, [rsi+8]
    L0063: mov eax, [rdx+8]
    L0066: cmp eax, ecx
    L0068: jbe L010f
    L006e: mov ecx, ecx
    L0070: mov word ptr [rdx+rcx*2+0x10], 0x20
    L0077: inc dword ptr [rsi+0x18]
    L007a: mov rdx, [rbx+0x10]
    L007e: test rdx, rdx
    L0081: je short L0094
    L0083: mov r8d, [rdx+8]
    L0087: add rdx, 0xc
    L008b: mov rcx, rsi
    L008e: call qword ptr [0x7ff93e6dd980]
    L0094: mov ecx, [rsi+0x18]
    L0097: mov rdx, [rsi+8]
    L009b: mov eax, [rdx+8]
    L009e: cmp eax, ecx
    L00a0: jbe L0122
    L00a6: mov ecx, ecx
    L00a8: mov word ptr [rdx+rcx*2+0x10], 0x20
    L00af: inc dword ptr [rsi+0x18]
    L00b2: mov rdx, [rbx+0x18]
    L00b6: test rdx, rdx
    L00b9: je short L00cc
    L00bb: mov r8d, [rdx+8]
    L00bf: add rdx, 0xc
    L00c3: mov rcx, rsi
    L00c6: call qword ptr [0x7ff93e6dd980]
    L00cc: mov ecx, [rsi+0x18]
    L00cf: mov rdx, [rsi+8]
    L00d3: mov eax, [rdx+8]
    L00d6: cmp eax, ecx
    L00d8: jbe short L0132
    L00da: mov ecx, ecx
    L00dc: mov word ptr [rdx+rcx*2+0x10], 0x20
    L00e3: inc dword ptr [rsi+0x18]
    L00e6: mov rdx, [rbx+0x20]
    L00ea: test rdx, rdx
    L00ed: je short L0100
    L00ef: mov r8d, [rdx+8]
    L00f3: add rdx, 0xc
    L00f7: mov rcx, rsi
    L00fa: call qword ptr [0x7ff93e6dd980]
    L0100: mov rcx, rsi
    L0103: add rsp, 0x28
    L0107: pop rbx
    L0108: pop rsi
    L0109: jmp qword ptr [0x7ff93e6e2a28]
    L010f: mov rcx, rsi
    L0112: mov edx, 0x20
    L0117: call qword ptr [0x7ff93e6dd2f0]
    L011d: jmp L007a
    L0122: mov rcx, rsi
    L0125: mov edx, 0x20
    L012a: call qword ptr [0x7ff93e6dd2f0]
    L0130: jmp short L00b2
    L0132: mov rcx, rsi
    L0135: mov edx, 0x20
    L013a: call qword ptr [0x7ff93e6dd2f0]
    L0140: jmp short L00e6
// .NET 8 (X64)
StringBuilderExact24()
    L0000: push rsi
    L0001: push rbx
    L0002: sub rsp, 0x28
    L0006: mov rbx, rcx
    L0009: mov rcx, 0x7ff93e6e29c0
    L0013: call 0x00007ff99e0fae10
    L0018: mov rsi, rax
    L001b: mov rcx, rsi
    L001e: mov edx, 0x18
    L0023: mov r8d, 0x7fffffff
    L0029: call qword ptr [0x7ff93e6dcff0]
    L002f: mov rdx, [rbx+8]
    L0033: test rdx, rdx
    L0036: je short L0049
    L0038: mov r8d, [rdx+8]
    L003c: add rdx, 0xc
    L0040: mov rcx, rsi
    L0043: call qword ptr [0x7ff93e6dd980]
    L0049: mov ecx, [rsi+0x18]
    L004c: mov rdx, [rsi+8]
    L0050: mov eax, [rdx+8]
    L0053: cmp eax, ecx
    L0055: jbe L00fc
    L005b: mov ecx, ecx
    L005d: mov word ptr [rdx+rcx*2+0x10], 0x20
    L0064: inc dword ptr [rsi+0x18]
    L0067: mov rdx, [rbx+0x10]
    L006b: test rdx, rdx
    L006e: je short L0081
    L0070: mov r8d, [rdx+8]
    L0074: add rdx, 0xc
    L0078: mov rcx, rsi
    L007b: call qword ptr [0x7ff93e6dd980]
    L0081: mov ecx, [rsi+0x18]
    L0084: mov rdx, [rsi+8]
    L0088: mov eax, [rdx+8]
    L008b: cmp eax, ecx
    L008d: jbe L010f
    L0093: mov ecx, ecx
    L0095: mov word ptr [rdx+rcx*2+0x10], 0x20
    L009c: inc dword ptr [rsi+0x18]
    L009f: mov rdx, [rbx+0x18]
    L00a3: test rdx, rdx
    L00a6: je short L00b9
    L00a8: mov r8d, [rdx+8]
    L00ac: add rdx, 0xc
    L00b0: mov rcx, rsi
    L00b3: call qword ptr [0x7ff93e6dd980]
    L00b9: mov ecx, [rsi+0x18]
    L00bc: mov rdx, [rsi+8]
    L00c0: mov eax, [rdx+8]
    L00c3: cmp eax, ecx
    L00c5: jbe short L011f
    L00c7: mov ecx, ecx
    L00c9: mov word ptr [rdx+rcx*2+0x10], 0x20
    L00d0: inc dword ptr [rsi+0x18]
    L00d3: mov rdx, [rbx+0x20]
    L00d7: test rdx, rdx
    L00da: je short L00ed
    L00dc: mov r8d, [rdx+8]
    L00e0: add rdx, 0xc
    L00e4: mov rcx, rsi
    L00e7: call qword ptr [0x7ff93e6dd980]
    L00ed: mov rcx, rsi
    L00f0: add rsp, 0x28
    L00f4: pop rbx
    L00f5: pop rsi
    L00f6: jmp qword ptr [0x7ff93e6e2a28]
    L00fc: mov rcx, rsi
    L00ff: mov edx, 0x20
    L0104: call qword ptr [0x7ff93e6dd2f0]
    L010a: jmp L0067
    L010f: mov rcx, rsi
    L0112: mov edx, 0x20
    L0117: call qword ptr [0x7ff93e6dd2f0]
    L011d: jmp short L009f
    L011f: mov rcx, rsi
    L0122: mov edx, 0x20
    L0127: call qword ptr [0x7ff93e6dd2f0]
    L012d: jmp short L00d3
// .NET 8 (X64)
StringBuilderEstimate100()
    L0000: push rsi
    L0001: push rbx
    L0002: sub rsp, 0x28
    L0006: mov rbx, rcx
    L0009: mov rcx, 0x7ff93e6e29c0
    L0013: call 0x00007ff99e0fae10
    L0018: mov rsi, rax
    L001b: mov rcx, rsi
    L001e: mov edx, 0x64
    L0023: mov r8d, 0x7fffffff
    L0029: call qword ptr [0x7ff93e6dcff0]
    L002f: mov rdx, [rbx+8]
    L0033: test rdx, rdx
    L0036: je short L0049
    L0038: mov r8d, [rdx+8]
    L003c: add rdx, 0xc
    L0040: mov rcx, rsi
    L0043: call qword ptr [0x7ff93e6dd980]
    L0049: mov ecx, [rsi+0x18]
    L004c: mov rdx, [rsi+8]
    L0050: mov eax, [rdx+8]
    L0053: cmp eax, ecx
    L0055: jbe L00fc
    L005b: mov ecx, ecx
    L005d: mov word ptr [rdx+rcx*2+0x10], 0x20
    L0064: inc dword ptr [rsi+0x18]
    L0067: mov rdx, [rbx+0x10]
    L006b: test rdx, rdx
    L006e: je short L0081
    L0070: mov r8d, [rdx+8]
    L0074: add rdx, 0xc
    L0078: mov rcx, rsi
    L007b: call qword ptr [0x7ff93e6dd980]
    L0081: mov ecx, [rsi+0x18]
    L0084: mov rdx, [rsi+8]
    L0088: mov eax, [rdx+8]
    L008b: cmp eax, ecx
    L008d: jbe L010f
    L0093: mov ecx, ecx
    L0095: mov word ptr [rdx+rcx*2+0x10], 0x20
    L009c: inc dword ptr [rsi+0x18]
    L009f: mov rdx, [rbx+0x18]
    L00a3: test rdx, rdx
    L00a6: je short L00b9
    L00a8: mov r8d, [rdx+8]
    L00ac: add rdx, 0xc
    L00b0: mov rcx, rsi
    L00b3: call qword ptr [0x7ff93e6dd980]
    L00b9: mov ecx, [rsi+0x18]
    L00bc: mov rdx, [rsi+8]
    L00c0: mov eax, [rdx+8]
    L00c3: cmp eax, ecx
    L00c5: jbe short L011f
    L00c7: mov ecx, ecx
    L00c9: mov word ptr [rdx+rcx*2+0x10], 0x20
    L00d0: inc dword ptr [rsi+0x18]
    L00d3: mov rdx, [rbx+0x20]
    L00d7: test rdx, rdx
    L00da: je short L00ed
    L00dc: mov r8d, [rdx+8]
    L00e0: add rdx, 0xc
    L00e4: mov rcx, rsi
    L00e7: call qword ptr [0x7ff93e6dd980]
    L00ed: mov rcx, rsi
    L00f0: add rsp, 0x28
    L00f4: pop rbx
    L00f5: pop rsi
    L00f6: jmp qword ptr [0x7ff93e6e2a28]
    L00fc: mov rcx, rsi
    L00ff: mov edx, 0x20
    L0104: call qword ptr [0x7ff93e6dd2f0]
    L010a: jmp L0067
    L010f: mov rcx, rsi
    L0112: mov edx, 0x20
    L0117: call qword ptr [0x7ff93e6dd2f0]
    L011d: jmp short L009f
    L011f: mov rcx, rsi
    L0122: mov edx, 0x20
    L0127: call qword ptr [0x7ff93e6dd2f0]
    L012d: jmp short L00d3


Benchmark Description:


Benchmark showing how setting initial capacity for a StringBuilder can improve performance. Mostly if we are using SB we likely don't know the final capacity, but as we can see with StringBuilderEstimate100 benchmark above even a way-off estimate can improve speed, albeit at the cost of more memory. The perf benefit comes from the fact that the array which the SB uses internally needs less resizing.

The provided benchmark code is designed to measure and compare the performance of constructing strings using the `StringBuilder` class in C# with different initial capacities. It is set up to run under the .NET 8.0 runtime environment, indicating it targets the latest or one of the latest versions of the .NET runtime as of the knowledge cutoff in 2023. The benchmarks are configured to hide certain columns in the output for clarity and focus on the most relevant metrics. The use of the `MemoryDiagnoser` attribute indicates that memory allocation is also being measured and reported, which is crucial for understanding the memory efficiency of the different approaches. ### General Setup - **.NET Version**: The benchmarks are specified to run on .NET 8.0 (`[SimpleJob(RuntimeMoniker.Net80)]`), ensuring the tests are executed using the latest runtime optimizations and features available in .NET. - **Configuration**: Custom configuration is provided to adjust the summary style, particularly to display ratio styles in percentage. This makes it easier to compare the relative performance differences between the benchmark methods. - **Memory Diagnoser**: Enabled to report on memory allocations, which helps in understanding the memory efficiency of different `StringBuilder` initialization strategies. ### Benchmark Methods #### 1. `StringBuilder()` - **Purpose**: This method serves as the baseline for comparison. It constructs a `StringBuilder` instance without specifying an initial capacity. - **Performance Aspect**: It tests the default behavior of `StringBuilder` when appending strings without pre-allocating memory. This scenario is common in many applications where the final string length is unknown upfront. - **Expected Insights**: This benchmark will show how the `StringBuilder` performs in terms of speed and memory allocation when it needs to automatically resize its internal buffer to accommodate appended strings. #### 2. `StringBuilderExact24()` - **Purpose**: This method tests the performance of `StringBuilder` when initialized with an exact capacity needed for the final string. - **Performance Aspect**: It specifically measures the impact of initializing `StringBuilder` with a capacity that perfectly matches the length of the final string, thus avoiding any resizing operations. - **Expected Insights**: The results should indicate whether pre-allocating the exact memory needed can improve performance and reduce memory allocations compared to the baseline. This method is expected to be more efficient since it eliminates the need for buffer resizing. #### 3. `StringBuilderEstimate100()` - **Purpose**: This method evaluates the performance when `StringBuilder` is initialized with a capacity larger than needed. - **Performance Aspect**: It assesses how over-estimating the initial capacity affects performance and memory usage, testing the scenario where developers allocate more memory upfront to avoid resizing, possibly at the expense of using more memory than necessary. - **Expected Insights**: The benchmark will show if and how much performance gain or loss is associated with over-allocating memory. While it may reduce or eliminate resizing operations, it could lead to higher memory usage, which might not be desirable in memory-constrained environments. ### Conclusion Running these benchmarks will provide insights into the trade-offs between initializing `StringBuilder` with different capacities. It will help developers understand how to balance between memory efficiency and performance, especially in scenarios where `StringBuilder` is heavily used for string manipulation. The results will guide best practices for using `StringBuilder` in terms of specifying initial capacities to optimize for speed and memory usage.


Benchmark Comments: