String.Replace performance improvements in .NET 7 v .NET 6




Date Added (UTC):

08 Apr 2024 @ 01:32

Date Updated (UTC):

08 Apr 2024 @ 01:32


.NET Version(s):

.NET 6 .NET 7

Tag(s):

#.Net7PerfImprovement #StringManipulation #Strings


Added By:
Profile Image

Blog   
Ireland    
.NET Developer and tech lead from Ireland!

Benchmark Results:





Benchmark Code:



using System.Net.Http;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;

namespace BenchmarkDotNet.Samples
{
    [Config(typeof(Config))]
    [SimpleJob(RuntimeMoniker.Net60, baseline: true)]
    [SimpleJob(RuntimeMoniker.Net70)]
    [HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio)]
    public class StringReplaceBenchmarks
    {
        private string _str;

        [GlobalSetup]
        public async Task Setup()
        {
            using var hc = new HttpClient();
            _str = await
                hc.GetStringAsync("https://www.gutenberg.org/cache/epub/3200/pg3200.txt");

            // The Entire Project Gutenberg Works of Mark Twain
        }

        [Benchmark]
        public string Yell() => _str.Replace(".", "!");

        [Benchmark]
        public string ConcatLines() => _str.Replace("\n", "");

        [Benchmark]
        public string NormalizeEndings() => _str.Replace("\r\n", "\n");

        [Benchmark]
        public string Ireland() => _str.Replace("Ireland", "IRELAND"); //19 occurrences

        private class Config : ManualConfig
        {
            public Config()
            {
                SummaryStyle =
                    SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
            }
        }
    }
}

// .NET 6, .NET 7
public string Yell()
{
    return _str.Replace(".", "!");
}
// .NET 6, .NET 7
public string ConcatLines()
{
    return _str.Replace("\n", "");
}
// .NET 6, .NET 7
public string NormalizeEndings()
{
    return _str.Replace("\r\n", "\n");
}
// .NET 6, .NET 7
public string Ireland()
{
    return _str.Replace("Ireland", "IRELAND");
}

// .NET 6, .NET 7
.method public hidebysig 
    instance string Yell () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 28 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20d3
    // Code size 22 (0x16)
    .maxstack 8

    // sequence point: (line 41, col 33) to (line 41, col 55) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string BenchmarkDotNet.Samples.StringReplaceBenchmarks::_str
    IL_0006: ldstr "."
    IL_000b: ldstr "!"
    IL_0010: callvirt instance string [System.Runtime]System.String::Replace(string, string)
    IL_0015: ret
}
// .NET 6, .NET 7
.method public hidebysig 
    instance string ConcatLines () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2b 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20ea
    // Code size 22 (0x16)
    .maxstack 8

    // sequence point: (line 44, col 40) to (line 44, col 62) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string BenchmarkDotNet.Samples.StringReplaceBenchmarks::_str
    IL_0006: ldstr "\n"
    IL_000b: ldstr ""
    IL_0010: callvirt instance string [System.Runtime]System.String::Replace(string, string)
    IL_0015: ret
}
// .NET 6, .NET 7
.method public hidebysig 
    instance string NormalizeEndings () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 2e 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2101
    // Code size 22 (0x16)
    .maxstack 8

    // sequence point: (line 47, col 45) to (line 47, col 71) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string BenchmarkDotNet.Samples.StringReplaceBenchmarks::_str
    IL_0006: ldstr "\r\n"
    IL_000b: ldstr "\n"
    IL_0010: callvirt instance string [System.Runtime]System.String::Replace(string, string)
    IL_0015: ret
}
// .NET 6, .NET 7
.method public hidebysig 
    instance string Ireland () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 31 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2118
    // Code size 22 (0x16)
    .maxstack 8

    // sequence point: (line 50, col 36) to (line 50, col 70) in _
    IL_0000: ldarg.0
    IL_0001: ldfld string BenchmarkDotNet.Samples.StringReplaceBenchmarks::_str
    IL_0006: ldstr "Ireland"
    IL_000b: ldstr "IRELAND"
    IL_0010: callvirt instance string [System.Runtime]System.String::Replace(string, string)
    IL_0015: ret
}

// .NET 6 (X64)
Yell()
    L0000: mov rcx, [rcx+8]
    L0004: mov rdx, 0x23bbbac3108
    L000e: mov rdx, [rdx]
    L0011: mov r8, 0x23bbbb0ccd8
    L001b: mov r8, [r8]
    L001e: cmp [rcx], ecx
    L0020: jmp System.String.Replace(System.String, System.String)
// .NET 7 (X64)
Yell()
    L0000: mov rcx, [rcx+8]
    L0004: mov rdx, 0x23e65402170
    L000e: mov rdx, [rdx]
    L0011: mov r8, 0x23e6544bed0
    L001b: mov r8, [r8]
    L001e: cmp [rcx], ecx
    L0020: jmp qword ptr [0x7ffbce7130d8]
// .NET 6 (X64)
ConcatLines()
    L0000: mov rcx, [rcx+8]
    L0004: mov rdx, 0x23bbbaebb58
    L000e: mov rdx, [rdx]
    L0011: mov r8, 0x23bbbac3020
    L001b: mov r8, [r8]
    L001e: cmp [rcx], ecx
    L0020: jmp System.String.Replace(System.String, System.String)
// .NET 7 (X64)
ConcatLines()
    L0000: mov rcx, [rcx+8]
    L0004: mov rdx, 0x23e6542b4e0
    L000e: mov rdx, [rdx]
    L0011: mov r8, 0x23e65402028
    L001b: mov r8, [r8]
    L001e: cmp [rcx], ecx
    L0020: jmp qword ptr [0x7ffbce7130d8]
// .NET 6 (X64)
NormalizeEndings()
    L0000: mov rcx, [rcx+8]
    L0004: mov rdx, 0x23bbbac3408
    L000e: mov rdx, [rdx]
    L0011: mov r8, 0x23bbbaebb58
    L001b: mov r8, [r8]
    L001e: cmp [rcx], ecx
    L0020: jmp System.String.Replace(System.String, System.String)
// .NET 7 (X64)
NormalizeEndings()
    L0000: mov rcx, [rcx+8]
    L0004: mov rdx, 0x23e654023f8
    L000e: mov rdx, [rdx]
    L0011: mov r8, 0x23e6542b4e0
    L001b: mov r8, [r8]
    L001e: cmp [rcx], ecx
    L0020: jmp qword ptr [0x7ffbce7130d8]
// .NET 6 (X64)
Ireland()
    L0000: mov rcx, [rcx+8]
    L0004: mov rdx, 0x23bbbb514e0
    L000e: mov rdx, [rdx]
    L0011: mov r8, 0x23bbbb514e8
    L001b: mov r8, [r8]
    L001e: cmp [rcx], ecx
    L0020: jmp System.String.Replace(System.String, System.String)
// .NET 7 (X64)
Ireland()
    L0000: mov rcx, [rcx+8]
    L0004: mov rdx, 0x23e654903f0
    L000e: mov rdx, [rdx]
    L0011: mov r8, 0x23e654903f8
    L001b: mov r8, [r8]
    L001e: cmp [rcx], ecx
    L0020: jmp qword ptr [0x7ffbce7130d8]


Benchmark Description:


There was some amazing String.Replace performance improvements in .NET 7.

The provided benchmark code is designed to measure the performance of different string manipulation operations in .NET, specifically targeting the .NET 6.0 and .NET 7.0 runtimes. The benchmarks are set up using the BenchmarkDotNet library, a powerful tool for benchmarking .NET code, which provides detailed insights into the performance characteristics of the tested methods. ### General Setup - **.NET Versions:** The benchmarks are configured to run against two different runtime versions: .NET 6.0 (as the baseline) and .NET 7.0. This allows for a comparison of performance across these versions. - **Configuration:** The `Config` class customizes the output of the benchmark results, specifically by adjusting the summary style to display ratios in percentage format. This makes it easier to understand the relative performance differences. - **BenchmarkDotNet Attributes:** - `@Config`: Specifies the configuration class to use for the benchmark. - `@SimpleJob`: Defines jobs for .NET 6.0 (as baseline) and .NET 7.0, indicating that the benchmarks will be run on these two runtime versions. - `@HideColumns`: Hides specific columns in the output report for clarity, such as the job, ratio standard deviation, and allocation ratio columns. ### Benchmark Methods 1. **Yell Method:** - **Purpose:** This method tests the performance of replacing all occurrences of a single character (in this case, a period ".") with another character (an exclamation mark "!"). - **Performance Aspect:** It measures how efficiently the runtime can perform a simple, character-based replacement operation on a large string. This is a common operation in text processing, making its performance critical for applications that involve heavy text manipulation. - **Expected Insights:** The results will show how quickly each runtime version can complete this operation, providing insights into the efficiency of character replacement in large strings. 2. **ConcatLines Method:** - **Purpose:** This method benchmarks the performance of removing all newline characters from a string. - **Performance Aspect:** It evaluates the runtime's ability to handle string replacements that remove characters, which is a common requirement for preprocessing text data. - **Expected Insights:** The benchmark will reveal the speed and efficiency of each .NET version in processing and manipulating large text blocks by removing specific characters. 3. **NormalizeEndings Method:** - **Purpose:** The goal here is to measure the performance of replacing carriage return-line feed pairs ("\r\n") with a single line feed character ("\n"). - **Performance Aspect:** This operation is important for normalizing newline characters across different environments, a common task when dealing with text data from various sources. - **Expected Insights:** Insights from this benchmark will indicate how well and quickly each runtime can perform this normalization task, which is crucial for cross-platform text processing applications. 4. **Ireland Method:** - **Purpose:** This method tests the performance of replacing a specific word ("Ireland") with its uppercase version ("IRELAND"), where the word occurs 19 times in the text. - **Performance Aspect:** It assesses the runtime's efficiency in performing case-sensitive, substring replacement operations within a large text body. - **Expected Insights:** The results will show the capability of each .NET version to handle word-based replacements in text, providing an understanding of the performance implications of such operations in applications that require dynamic text manipulation. ### Conclusion By running these benchmarks, developers can gain valuable insights into the performance characteristics of different string manipulation operations across .NET 6.0 and .NET 7.0. This information can guide optimization efforts and help in making informed decisions when choosing between different approaches to text processing in .NET applications.


Benchmark Comments: