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): Tag(s):
#StringBuilder #StringConcatenation #Strings
Added By:
.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);
}
}
}
}
Powered by SharpLab
// .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();
}
Powered by SharpLab
// .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
}
Powered by SharpLab
|
|
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.