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): Tag(s):
#.Net8PerfImprovement #StringBuilder #StringConcatenation
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)]
[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);
}
}
}
}
Powered by SharpLab
// .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();
}
Powered by SharpLab
// .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
}
Powered by SharpLab
|
|
|
|
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.