Skip to content

Peculiar string operation performance #13451

@Suchiman

Description

@Suchiman

So i got nerd sniped on IRC with how fast you can append characters to create a string.
I created the following benchmark to measure a few alternatives just before i got told it's for an old MVC4 app. Then I've noticed that the performance is consistently worse on .NET Core, in particular for StringBuilder and even more so if you supply a capacity for StringBuilder. I then suspected Tiered Compilation but i couldn't figure how to really disable it for BDN.NET, <TieredCompilation>false</TieredCompilation> had at best an effect on the InProcess Benchmark. Also it appears that BDN.NET or the API it is using for measuring the Allocated number is incorrect on .NET Core, the .NET Framework numbers seem more plausible (allocating an char[] of 50k elements and a string from that char[] certainly doesn't allocate exactly as much as just allocating the string).

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text;

namespace ConsoleApp37
{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<ConcatRace>();
        }
    }

    //[InProcess]
    [MemoryDiagnoser]
    public class ConcatRace
    {
        const int length = 50000;

        [Benchmark]
        public string StringBuilderAddChar()
        {
            var result = new StringBuilder();

            for (int i = 0; i < length; i++)
            {
                result.Append('s');
            }

            return result.ToString();
        }

        [Benchmark]
        public string StringBuilderAddCharOptimized()
        {
            var result = new StringBuilder(length);

            for (int i = 0; i < length; i++)
            {
                result.Append('s');
            }

            return result.ToString();
        }

        [Benchmark]
        public unsafe string PointerAddChar()
        {
            var result = new char[length];

            fixed (char* fixedPointer = result)
            {
                var pointer = fixedPointer;
                for (int i = 0; i < length; i++)
                {
                    *pointer++ = 's';
                }
            }

            return new string(result);
        }

        [Benchmark]
        public unsafe string NormalAddChar()
        {
            var result = new char[length];

            for (int i = 0; i < result.Length; i++)
            {
                result[i] = 's';
            }

            return new string(result);
        }

        [Benchmark]
        public unsafe string FastPointerAddChar()
        {
            var result = new string('\0', length);

            fixed (char* fixedPointer = result)
            {
                var pointer = fixedPointer;
                for (int i = 0; i < length; i++)
                {
                    *pointer++ = 's';
                }
            }

            return result;
        }
    }
}

.NET Framework 4.7.2

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-4670K CPU 3.40GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
  [Host]     : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.8.3815.0
  DefaultJob : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.8.3815.0

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
StringBuilderAddChar 109.43 us 0.3330 us 0.2952 us 62.3779 31.1279 31.1279 209.02 KB
StringBuilderAddCharOptimized 100.69 us 0.5440 us 0.5089 us 62.3779 62.3779 62.3779 195.87 KB
PointerAddChar 33.79 us 0.2182 us 0.1934 us 62.4390 62.4390 62.4390 195.37 KB
NormalAddChar 38.40 us 0.1843 us 0.1539 us 62.4390 62.4390 62.4390 195.37 KB
FastPointerAddChar 24.62 us 0.2241 us 0.2096 us 31.2195 31.2195 31.2195 97.69 KB

.NET Core 3.0 RC1 (OutOfProcess)

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-4670K CPU 3.40GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=3.0.100-rc1-014190
  [Host]     : .NET Core 3.0.0-rc1-19456-20 (CoreCLR 4.700.19.45506, CoreFX 4.700.19.45604), 64bit RyuJIT
  DefaultJob : .NET Core 3.0.0-rc1-19456-20 (CoreCLR 4.700.19.45506, CoreFX 4.700.19.45604), 64bit RyuJIT

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
StringBuilderAddChar 120.80 us 0.5277 us 0.4936 us 62.2559 31.0059 31.0059 208.55 KB
StringBuilderAddCharOptimized 142.44 us 1.1631 us 1.0879 us 62.2559 62.2559 62.2559 97.73 KB
PointerAddChar 35.33 us 0.2321 us 0.2057 us 62.4390 62.4390 62.4390 97.68 KB
NormalAddChar 40.27 us 0.4038 us 0.3777 us 62.4390 62.4390 62.4390 97.68 KB
FastPointerAddChar 29.78 us 0.1331 us 0.1245 us 31.2195 31.2195 31.2195 97.68 KB

.NET Core 3.0 RC1 (InProcess)

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-4670K CPU 3.40GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=3.0.100-rc1-014190
  [Host] : .NET Core 3.0.0-rc1-19456-20 (CoreCLR 4.700.19.45506, CoreFX 4.700.19.45604), 64bit RyuJIT

Job=InProcess  Toolchain=InProcessEmitToolchain  
Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
StringBuilderAddChar 121.68 us 0.4015 us 0.3352 us 62.3779 31.1279 31.1279 208.55 KB
StringBuilderAddCharOptimized 111.83 us 1.4062 us 1.3154 us 62.3779 62.3779 62.3779 97.73 KB
PointerAddChar 43.64 us 0.5115 us 0.4784 us 62.4390 62.4390 62.4390 97.68 KB
NormalAddChar 48.63 us 0.6182 us 0.5782 us 62.4390 62.4390 62.4390 97.68 KB
FastPointerAddChar 29.98 us 0.3981 us 0.3724 us 31.2195 31.2195 31.2195 97.68 KB

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions