.NET 8 new SearchValues for counting line endings benchmarks




Date Added (UTC):

15 Apr 2024 @ 21:01

Date Updated (UTC):

19 Apr 2024 @ 01:02


.NET Version(s):

.NET 8

Tag(s):

#.Net8PerfImprovement #Strings


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.Environments;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Reports;
using System.Buffers;
using System.Net.Http;
using System;

[HideColumns("Error", "StdDev", "Median", "RatioSD")]
[MemoryDiagnoser(displayGenColumns: false)]
[Config(typeof(Config))]
public class SearchValues
{
    private static readonly string s_haystack =
        new HttpClient().GetStringAsync("https://www.gutenberg.org/files/1661/1661-0.txt").Result;

    private static readonly SearchValues<char> s_lineEndings =
        System.Buffers.SearchValues.Create("\n\r\f\u0085\u2028\u2029");

    [Benchmark]
    public int CountLineEndings_Chars()
    {
        int count = 0;
        ReadOnlySpan<char> haystack = s_haystack;

        int pos;
        while ((pos = haystack.IndexOfAny("\n\r\f\u0085\u2028\u2029")) >= 0)
        {
            count++;
            haystack = haystack.Slice(pos + 1);
        }

        return count;
    }

    [Benchmark]
    public int CountLineEndings_SearchValues()
    {
        int count = 0;
        ReadOnlySpan<char> haystack = s_haystack;

        int pos;
        while ((pos = haystack.IndexOfAny(s_lineEndings)) >= 0)
        {
            count++;
            haystack = haystack.Slice(pos + 1);
        }

        return count;
    }

    private class Config : ManualConfig
    {
        public Config()
        {
            AddJob(Job.Default.WithId(".NET 8").WithRuntime(CoreRuntime.Core80));

            SummaryStyle =
                SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
        }
    }
}

// .NET 8
public int CountLineEndings_Chars()
{
    int num = 0;
    ReadOnlySpan<char> span = s_haystack;
    int num2;
    while ((num2 = MemoryExtensions.IndexOfAny(span, "\n\r\f\u0085\u2028\u2029")) >= 0)
    {
        num++;
        span = span.Slice(num2 + 1);
    }
    return num;
}
// .NET 8
public int CountLineEndings_SearchValues()
{
    int num = 0;
    ReadOnlySpan<char> span = s_haystack;
    int num2;
    while ((num2 = MemoryExtensions.IndexOfAny(span, s_lineEndings)) >= 0)
    {
        num++;
        span = span.Slice(num2 + 1);
    }
    return num;
}

// .NET 8
.method public hidebysig 
    instance int32 CountLineEndings_Chars () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 21 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2050
    // Code size 53 (0x35)
    .maxstack 3
    .locals init (
        [0] int32 count,
        [1] valuetype [System.Runtime]System.ReadOnlySpan`1<char> haystack,
        [2] int32 pos
    )

    // sequence point: (line 36, col 9) to (line 36, col 23) in _
    IL_0000: ldc.i4.0
    IL_0001: stloc.0
    // sequence point: (line 37, col 9) to (line 37, col 50) in _
    IL_0002: ldsfld string SearchValues::s_haystack
    IL_0007: call valuetype [System.Runtime]System.ReadOnlySpan`1<char> [System.Runtime]System.String::op_Implicit(string)
    IL_000c: stloc.1
    // sequence point: hidden
    IL_000d: br.s IL_001e
    // loop start (head: IL_001e)
        // sequence point: (line 42, col 13) to (line 42, col 21) in _
        IL_000f: ldloc.0
        IL_0010: ldc.i4.1
        IL_0011: add
        IL_0012: stloc.0
        // sequence point: (line 43, col 13) to (line 43, col 48) in _
        IL_0013: ldloca.s 1
        IL_0015: ldloc.2
        IL_0016: ldc.i4.1
        IL_0017: add
        IL_0018: call instance valuetype [System.Runtime]System.ReadOnlySpan`1<!0> valuetype [System.Runtime]System.ReadOnlySpan`1<char>::Slice(int32)
        IL_001d: stloc.1

        // sequence point: (line 40, col 9) to (line 40, col 77) in _
        IL_001e: ldloc.1
        IL_001f: ldstr "\n\r\f\u0085\u2028\u2029"
        IL_0024: call valuetype [System.Runtime]System.ReadOnlySpan`1<char> [System.Runtime]System.String::op_Implicit(string)
        IL_0029: call int32 [System.Memory]System.MemoryExtensions::IndexOfAny<char>(valuetype [System.Runtime]System.ReadOnlySpan`1<!!0>, valuetype [System.Runtime]System.ReadOnlySpan`1<!!0>)
        IL_002e: dup
        IL_002f: stloc.2
        IL_0030: ldc.i4.0
        IL_0031: bge.s IL_000f
    // end loop

    // sequence point: (line 46, col 9) to (line 46, col 22) in _
    IL_0033: ldloc.0
    IL_0034: ret
}
// .NET 8
.method public hidebysig 
    instance int32 CountLineEndings_SearchValues () 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 0x2094
    // Code size 48 (0x30)
    .maxstack 3
    .locals init (
        [0] int32 count,
        [1] valuetype [System.Runtime]System.ReadOnlySpan`1<char> haystack,
        [2] int32 pos
    )

    // sequence point: (line 52, col 9) to (line 52, col 23) in _
    IL_0000: ldc.i4.0
    IL_0001: stloc.0
    // sequence point: (line 53, col 9) to (line 53, col 50) in _
    IL_0002: ldsfld string SearchValues::s_haystack
    IL_0007: call valuetype [System.Runtime]System.ReadOnlySpan`1<char> [System.Runtime]System.String::op_Implicit(string)
    IL_000c: stloc.1
    // sequence point: hidden
    IL_000d: br.s IL_001e
    // loop start (head: IL_001e)
        // sequence point: (line 58, col 13) to (line 58, col 21) in _
        IL_000f: ldloc.0
        IL_0010: ldc.i4.1
        IL_0011: add
        IL_0012: stloc.0
        // sequence point: (line 59, col 13) to (line 59, col 48) in _
        IL_0013: ldloca.s 1
        IL_0015: ldloc.2
        IL_0016: ldc.i4.1
        IL_0017: add
        IL_0018: call instance valuetype [System.Runtime]System.ReadOnlySpan`1<!0> valuetype [System.Runtime]System.ReadOnlySpan`1<char>::Slice(int32)
        IL_001d: stloc.1

        // sequence point: (line 56, col 9) to (line 56, col 64) in _
        IL_001e: ldloc.1
        IL_001f: ldsfld class [System.Runtime]System.Buffers.SearchValues`1<char> SearchValues::s_lineEndings
        IL_0024: call int32 [System.Memory]System.MemoryExtensions::IndexOfAny<char>(valuetype [System.Runtime]System.ReadOnlySpan`1<!!0>, class [System.Runtime]System.Buffers.SearchValues`1<!!0>)
        IL_0029: dup
        IL_002a: stloc.2
        IL_002b: ldc.i4.0
        IL_002c: bge.s IL_000f
    // end loop

    // sequence point: (line 62, col 9) to (line 62, col 22) in _
    IL_002e: ldloc.0
    IL_002f: ret
}

// .NET 8 Jit Asm Code unavailable due to errors:
Type SearchValues has a static constructor, which is not supported by SharpLab JIT decompiler.


Benchmark Description:


[Performance Improvements in .NET 8](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/#searchvalues)

The provided benchmark code is designed to evaluate the performance of two different methods for counting line endings in a text document. It uses the BenchmarkDotNet library, a popular .NET benchmarking tool, to measure and compare the execution speed and memory usage of these methods. The setup and rationale for each part of the benchmark are detailed below. ### General Setup - **.NET Version**: The benchmark specifies the use of .NET 8 (CoreRuntime.Core80), indicating it is targeting the latest .NET runtime environment available at the time of writing. This ensures that the benchmarks are run using the most recent performance improvements and runtime optimizations. - **Configuration**: The `Config` class extends `ManualConfig` from BenchmarkDotNet, setting up a custom configuration for the benchmark runs. It specifies the use of `.NET 8` runtime and customizes the summary output to display ratios in percentage format. This configuration aims to provide clear and concise results focused on comparing the performance differences between the tested methods. - **Memory Diagnoser**: Enabled with `displayGenColumns: false` to report memory allocation without showing garbage collection generation columns. This focuses the memory diagnostics on the total allocated memory, which is often more relevant for performance comparisons. - **Columns**: The benchmark hides specific columns ("Error", "StdDev", "Median", "RatioSD") to streamline the output, focusing on the most relevant metrics for performance evaluation. ### Benchmark Methods #### 1. `CountLineEndings_Chars()` - **Purpose**: This method counts the number of line endings in a text document by searching for any of a set of line-ending characters (`"\n\r\f\u0085\u2028\u2029"`) using the `IndexOfAny` method on `ReadOnlySpan<char>`. - **Performance Aspect**: It tests the efficiency of using `IndexOfAny` with a direct character array to find line endings. This method represents a more traditional approach, relying on built-in methods without additional optimizations or data structures. - **Expected Insights**: The benchmark will reveal how quickly and with how much memory overhead the .NET runtime can perform this operation. It serves as a baseline for comparing the performance of more optimized approaches. #### 2. `CountLineEndings_SearchValues()` - **Purpose**: Similar to the first method, this one counts line endings but uses a `SearchValues<char>` instance (`s_lineEndings`) with the `IndexOfAny` method. `SearchValues` is designed to optimize searches for sets of values. - **Performance Aspect**: This method evaluates the performance benefits of using the `SearchValues` structure from `System.Buffers` for searching within a span. It's intended to test whether this approach can offer faster execution or reduced memory usage compared to the straightforward character array search. - **Expected Insights**: Results from this benchmark will indicate whether `SearchValues` provides a significant performance advantage over the basic character array approach. Improved performance or reduced memory allocation would justify the use of `SearchValues` in scenarios where such searches are common and performance-critical. ### Conclusion By running these benchmarks, you can gain insights into the efficiency of different methods for searching within character spans in .NET. Specifically, it will help understand the trade-offs between using simple character arrays versus specialized structures like `SearchValues` for common text processing tasks such as counting line endings. The benchmarks are designed to highlight differences in execution speed and memory usage, providing valuable data for making informed decisions about which approach to use in various scenarios.


Benchmark Comments: