Skip to content

Add StringBuilder.Append(ROM<char>) to existing Append(ROS<char>) for perf #27427

Closed
@eerhardt

Description

@eerhardt
using System;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Running;

namespace MyBenchmarks
{
    public class StringBuilderROM
    {
        private readonly ReadOnlyMemory<char> m1 = "xAbCdEfG".AsMemory().Slice(1);
        private readonly ReadOnlyMemory<char> m2 = "xAbCdEfGabcdefghijklmnopqrstuvwxyz".AsMemory().Slice(1);
        private readonly StringBuilder s = new StringBuilder();

        [Benchmark]
        public StringBuilder ShortAppendROM() => s.Clear().Append(m1);

        [Benchmark]
        public StringBuilder ShortAppendROS() => s.Clear().Append(m1.Span);

        [Benchmark]
        public StringBuilder LongAppendROM() => s.Clear().Append(m2);

        [Benchmark]
        public StringBuilder LongAppendROS() => s.Clear().Append(m2.Span);
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<StringBuilderROM>(DefaultConfig.Instance.With(MemoryDiagnoser.Default));
        }
    }
}

Results in:

BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17134.286 (1803/April2018Update/Redstone4)
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100-alpha1-20180720-2
  [Host]     : .NET Core 2.1.3 (CoreCLR 4.6.26725.06, CoreFX 4.6.26725.05), 64bit RyuJIT
  DefaultJob : .NET Core 2.1.3 (CoreCLR 4.6.26725.06, CoreFX 4.6.26725.05), 64bit RyuJIT


         Method |     Mean |     Error |    StdDev |  Gen 0 | Allocated |
--------------- |---------:|----------:|----------:|-------:|----------:|
 ShortAppendROM | 34.48 ns | 0.7666 ns | 0.9968 ns | 0.0171 |      72 B |
 ShortAppendROS | 20.06 ns | 0.2060 ns | 0.1927 ns |      - |       0 B |
  LongAppendROM | 45.51 ns | 0.8662 ns | 0.8103 ns | 0.0305 |     128 B |
  LongAppendROS | 20.78 ns | 0.1742 ns | 0.1629 ns |      - |       0 B |

The reason StringBuilder.Append(ROM<char>) even works is because Append has an overload that takes an object, which just turns around and calls .ToString() on the object. ROM<char> overrides the ToString, and returns string.Substring, which is going to make a copy of the string. Thus resulting in worse performance.

I'm sure I'm not the last person who is going to make this mistake assuming that StringBuilder.Append(ROM<char>) should "just work" as expected.

Proposal

A simple fix for this would be to add another overload to StringBuilder.Append that takes a ReadOnlyMemory<char> and just calls the existing Append(ReadOnlySpan<char>) method, which doesn't need to copy the string before appending.

namespace System.Text
{
    public class StringBuilder
    {
        public StringBuilder Append(ReadOnlyMemory<char> value);
    }
}

/cc @GrabYourPitchforks @ahsonkhan @codemzs

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-System.Runtimegood first issueIssue should be easy to implement, good for first-time contributorshelp wanted[up-for-grabs] Good issue for external contributorstenet-performancePerformance related issue

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions