Skip to content

Conversation

@stephentoub
Copy link
Member

The primary purpose of this change was to use IndexOfAny as part of parsing a composite string format in order to look for the next brace in the string: for longer composite formats with fewer holes, this can significantly speed up the parsing.

However, for very small strings filled with formats, the overhead of the IndexOfAny is unnecessary and resulted in regressions in some benchmarks, so I ended up overhauling the implementation to gain back the losses for those shorter cases, e.g. by avoiding bounds checking, by favoring expected cases, etc., and also generally cleaned up the implementation. As part of this, I also deleted the internal ParamsArray helper struct and replaced it with use of ReadOnlySpan<object?>.

Method Toolchain Mean Ratio
AppendFormat_Small_Strings \main\corerun.exe 51.18 ns 1.00
AppendFormat_Small_Strings \pr\corerun.exe 50.33 ns 0.98
AppendFormat_Medium_Strings \main\corerun.exe 65.40 ns 1.00
AppendFormat_Medium_Strings \pr\corerun.exe 54.49 ns 0.83
AppendFormat_Large_Strings \main\corerun.exe 193.19 ns 1.00
AppendFormat_Large_Strings \pr\corerun.exe 88.38 ns 0.46
AppendFormat_Varying \main\corerun.exe 188.89 ns 1.00
AppendFormat_Varying \pr\corerun.exe 180.35 ns 0.96
Format_Small_Strings \main\corerun.exe 66.31 ns 1.00
Format_Small_Strings \pr\corerun.exe 64.82 ns 0.98
Format_Medium_Strings \main\corerun.exe 86.25 ns 1.00
Format_Medium_Strings \pr\corerun.exe 68.47 ns 0.79
Format_Large_Strings \main\corerun.exe 237.92 ns 1.00
Format_Large_Strings \pr\corerun.exe 113.92 ns 0.48
Format_Varying \main\corerun.exe 201.46 ns 1.00
Format_Varying \pr\corerun.exe 197.43 ns 0.98
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;
using System.Text;

public class Program
{
    static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

    private StringBuilder _sb = new StringBuilder();
    private object _int = 12345;

    [Benchmark]
    public void AppendFormat_Small_Strings()
    {
        _sb.Clear();
        _sb.AppendFormat("{0}: {1}", "key", "value");
    }

    [Benchmark]
    public void AppendFormat_Medium_Strings()
    {
        _sb.Clear();
        _sb.AppendFormat("Key: {0}, Value: {1}", "key", "value");
    }

    [Benchmark]
    public void AppendFormat_Large_Strings()
    {
        _sb.Clear();
        _sb.AppendFormat("This is a test to see what happens when there's more text {0} the individual {1} that need to be {2} in.", "between", "arguments", "filled");
    }

    [Benchmark]
    public void AppendFormat_Varying()
    {
        _sb.Clear();
        _sb.AppendFormat("{0:X}.{1,10:X4}.{2,-4}.{3:d10}", _int, _int, _int, _int);
    }

    [Benchmark]
    public string Format_Small_Strings()
    {
        return string.Format("{0}: {1}", "key", "value");
    }

    [Benchmark]
    public string Format_Medium_Strings()
    {
        return string.Format("Key: {0}, Value: {1}", "key", "value");
    }

    [Benchmark]
    public string Format_Large_Strings()
    {
        return string.Format("This is a test to see what happens when there's more text {0} the individual {1} that need to be {2} in.", "between", "arguments", "filled");
    }

    [Benchmark]
    public string Format_Varying()
    {
        return string.Format("{0:X}.{1,10:X4}.{2,-4}.{3:d10}", _int, _int, _int, _int);
    }
}

…oughput

The primary purpose of this change was to use IndexOfAny as part of parsing a composite string format in order to look for the next brace in the string: for longer composite formats with fewer holes, this can significantly speed up the parsing.  However, for very small strings filled with formats, the overhead of the IndexOfAny is unnecessary, and so I ended up overhauling the implementation to gain back the losses for those shorter cases, e.g. by avoiding bounds checking, by favoring expected cases, etc., and also generally cleaned up the implementation.  As part of this, I also deleted the internal ParamsArray helper struct and replaced it with use of `ReadOnlySpan<object?>`.
@stephentoub stephentoub added this to the 7.0.0 milestone May 24, 2022
@ghost ghost assigned stephentoub May 24, 2022
@ghost
Copy link

ghost commented May 24, 2022

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

The primary purpose of this change was to use IndexOfAny as part of parsing a composite string format in order to look for the next brace in the string: for longer composite formats with fewer holes, this can significantly speed up the parsing.

However, for very small strings filled with formats, the overhead of the IndexOfAny is unnecessary and resulted in regressions in some benchmarks, so I ended up overhauling the implementation to gain back the losses for those shorter cases, e.g. by avoiding bounds checking, by favoring expected cases, etc., and also generally cleaned up the implementation. As part of this, I also deleted the internal ParamsArray helper struct and replaced it with use of ReadOnlySpan<object?>.

Method Toolchain Mean Ratio
AppendFormat_Small_Strings \main\corerun.exe 51.18 ns 1.00
AppendFormat_Small_Strings \pr\corerun.exe 50.33 ns 0.98
AppendFormat_Medium_Strings \main\corerun.exe 65.40 ns 1.00
AppendFormat_Medium_Strings \pr\corerun.exe 54.49 ns 0.83
AppendFormat_Large_Strings \main\corerun.exe 193.19 ns 1.00
AppendFormat_Large_Strings \pr\corerun.exe 88.38 ns 0.46
AppendFormat_Varying \main\corerun.exe 188.89 ns 1.00
AppendFormat_Varying \pr\corerun.exe 180.35 ns 0.96
Format_Small_Strings \main\corerun.exe 66.31 ns 1.00
Format_Small_Strings \pr\corerun.exe 64.82 ns 0.98
Format_Medium_Strings \main\corerun.exe 86.25 ns 1.00
Format_Medium_Strings \pr\corerun.exe 68.47 ns 0.79
Format_Large_Strings \main\corerun.exe 237.92 ns 1.00
Format_Large_Strings \pr\corerun.exe 113.92 ns 0.48
Format_Varying \main\corerun.exe 201.46 ns 1.00
Format_Varying \pr\corerun.exe 197.43 ns 0.98
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;
using System.Text;

public class Program
{
    static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);

    private StringBuilder _sb = new StringBuilder();
    private object _int = 12345;

    [Benchmark]
    public void AppendFormat_Small_Strings()
    {
        _sb.Clear();
        _sb.AppendFormat("{0}: {1}", "key", "value");
    }

    [Benchmark]
    public void AppendFormat_Medium_Strings()
    {
        _sb.Clear();
        _sb.AppendFormat("Key: {0}, Value: {1}", "key", "value");
    }

    [Benchmark]
    public void AppendFormat_Large_Strings()
    {
        _sb.Clear();
        _sb.AppendFormat("This is a test to see what happens when there's more text {0} the individual {1} that need to be {2} in.", "between", "arguments", "filled");
    }

    [Benchmark]
    public void AppendFormat_Varying()
    {
        _sb.Clear();
        _sb.AppendFormat("{0:X}.{1,10:X4}.{2,-4}.{3:d10}", _int, _int, _int, _int);
    }

    [Benchmark]
    public string Format_Small_Strings()
    {
        return string.Format("{0}: {1}", "key", "value");
    }

    [Benchmark]
    public string Format_Medium_Strings()
    {
        return string.Format("Key: {0}, Value: {1}", "key", "value");
    }

    [Benchmark]
    public string Format_Large_Strings()
    {
        return string.Format("This is a test to see what happens when there's more text {0} the individual {1} that need to be {2} in.", "between", "arguments", "filled");
    }

    [Benchmark]
    public string Format_Varying()
    {
        return string.Format("{0:X}.{1,10:X4}.{2,-4}.{3:d10}", _int, _int, _int, _int);
    }
}
Author: stephentoub
Assignees: -
Labels:

area-System.Runtime, tenet-performance

Milestone: 7.0.0

@stephentoub stephentoub merged commit 06a8b48 into dotnet:main Jun 6, 2022
@stephentoub stephentoub deleted the improvestringformat branch June 6, 2022 13:56
public StringBuilder AppendFormat([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, object? arg0, object? arg1, object? arg2)
{
ThreeObjects three = new ThreeObjects(arg0, arg1, arg2);
return AppendFormatHelper(null, format, MemoryMarshal.CreateReadOnlySpan(ref three.Arg0, 3));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to implement something like:

MemoryMarshal.CreateReadOnlySpan(ref arg2, 3)

Unreal?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants