Skip to content

Instantly share code, notes, and snippets.

@davepcallan
Created August 14, 2023 11:57
Show Gist options
  • Save davepcallan/64f3153efbd06fa7cfd84bc11a7d60d4 to your computer and use it in GitHub Desktop.
Save davepcallan/64f3153efbd06fa7cfd84bc11a7d60d4 to your computer and use it in GitHub Desktop.
Performance Sensitive simple string concatenation in .NET 8
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Reports;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Benchmarks
{
[MemoryDiagnoser]
[Config(typeof(Config))]
[SimpleJob(RuntimeMoniker.Net80)]
[HideColumns(Column.Job, Column.RatioSD, Column.AllocRatio)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[ReturnValueValidator(failOnError: true)]
[DisassemblyDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class PerfSensitiveStringConcatenation
{
private class Config : ManualConfig
{
public Config()
{
SummaryStyle =
SummaryStyle.Default.WithRatioStyle(RatioStyle.Percentage);
}
}
private string title = "Mr.", firstName = "David", middleName = "Patrick", lastName = "Callan";
[BenchmarkCategory("LengthUnknown"), Benchmark]
public string StringInterpolation() =>
$"{title} {firstName} {middleName} {lastName}";
[BenchmarkCategory("LengthUnknown"), Benchmark]
public string StringJoin() =>
string.Join(" ", new[] { title, firstName, middleName, lastName });
[BenchmarkCategory("LengthUnknown"), Benchmark]
public string StringCreate_TryWrite()
{
return string.Create(title.Length + firstName.Length + middleName.Length + lastName.Length + 3,
(title, firstName, middleName, lastName),
static (span, state) => span.TryWrite($"{state.title} {state.firstName} {state.middleName} {state.lastName}", out _));
}
[BenchmarkCategory("LengthUnknown"), Benchmark]
public string StringCreate_StringHandler() =>
string.Create(null, $"{title} {firstName} {middleName} {lastName}");
[BenchmarkCategory("LengthUnknown"), Benchmark(Baseline = true)]
public string StringCreate()
{
return string.Create(title.Length + firstName.Length + middleName.Length + lastName.Length + 3,
(title, firstName, middleName, lastName),
static (span, state) =>
{
state.title.AsSpan().CopyTo(span);
span = span.Slice(state.title.Length);
span[0] = ' ';
span = span.Slice(1);
state.firstName.AsSpan().CopyTo(span);
span = span.Slice(state.firstName.Length);
span[0] = ' ';
span = span.Slice(1);
state.middleName.AsSpan().CopyTo(span);
span = span.Slice(state.middleName.Length);
span[0] = ' ';
span = span.Slice(1);
state.lastName.AsSpan().CopyTo(span);
}
);
}
[BenchmarkCategory("FinalLengthKnown"), Benchmark]
public string StringCreate_StringHandler_SpanBuffer() =>
string.Create(null, stackalloc char[24], $"{title} {firstName} {middleName} {lastName}");
[BenchmarkCategory("FinalLengthKnown"), Benchmark]
public string StringCreate_TryWrite_constant()
{
return string.Create(24,
(title, firstName, middleName, lastName),
static (span, state) => span.TryWrite($"{state.title} {state.firstName} {state.middleName} {state.lastName}", out _));
}
[BenchmarkCategory("FinalLengthKnown"), Benchmark(Baseline = true)]
public string StringCreate_Constant()
{
return string.Create(24,
(title, firstName, middleName, lastName),
static (span, state) =>
{
state.title.AsSpan().CopyTo(span);
span = span.Slice(state.title.Length);
span[0] = ' ';
span = span.Slice(1);
state.firstName.AsSpan().CopyTo(span);
span = span.Slice(state.firstName.Length);
span[0] = ' ';
span = span.Slice(1);
state.middleName.AsSpan().CopyTo(span);
span = span.Slice(state.middleName.Length);
span[0] = ' ';
span = span.Slice(1);
state.lastName.AsSpan().CopyTo(span);
}
);
}
[BenchmarkCategory("FinalLengthKnown"), Benchmark]
public string RawSpan_Constant()
{
Span<char> buffer = stackalloc char[24];
var span = buffer;
title.AsSpan().CopyTo(span);
span = span.Slice(title.Length);
span[0] = ' ';
span = span.Slice(1);
firstName.AsSpan().CopyTo(span);
span = span.Slice(firstName.Length);
span[0] = ' ';
span = span.Slice(1);
middleName.AsSpan().CopyTo(span);
span = span.Slice(middleName.Length);
span[0] = ' ';
span = span.Slice(1);
lastName.AsSpan().CopyTo(span);
return new string(buffer);
}
[BenchmarkCategory("FinalLengthKnown"), Benchmark]
public string FastAllocateString_StringHandler()
{
var result = FastAllocateString(null, 24);
var buffer = MemoryMarshal.CreateSpan(ref GetRawStringData(result), 24);
Fill(null, buffer, $"{title} {firstName} {middleName} {lastName}");
return result;
}
[BenchmarkCategory("ExactLengthsKnown"), Benchmark]
public string StringCreate_More_Constant()
{
return string.Create(24,
(title, firstName, middleName, lastName),
static (span, state) =>
{
state.title.AsSpan().CopyTo(span);
span[3] = ' ';
span = span.Slice(4);
state.firstName.AsSpan().CopyTo(span);
span[5] = ' ';
span = span.Slice(6);
state.middleName.AsSpan().CopyTo(span);
span[7] = ' ';
span = span.Slice(8);
state.lastName.AsSpan().CopyTo(span);
}
);
}
[BenchmarkCategory("ExactLengthsKnown"), Benchmark]
public string RawSpan_More_Constant()
{
Span<char> buffer = stackalloc char[24];
var span = buffer;
title.AsSpan().CopyTo(span);
span[3] = ' ';
span = span.Slice(4);
firstName.AsSpan().CopyTo(span);
span[5] = ' ';
span = span.Slice(6);
middleName.AsSpan().CopyTo(span);
span[7] = ' ';
span = span.Slice(8);
lastName.AsSpan().CopyTo(span);
return new string(buffer);
}
[BenchmarkCategory("ExactLengthsKnown"), Benchmark(Baseline = true)]
public string FastAllocateString_Raw()
{
var result = FastAllocateString(null, 24);
var span = MemoryMarshal.CreateSpan(ref GetRawStringData(result), 24);
title.AsSpan().CopyTo(span);
span[3] = ' ';
span = span.Slice(4);
firstName.AsSpan().CopyTo(span);
span[5] = ' ';
span = span.Slice(6);
middleName.AsSpan().CopyTo(span);
span[7] = ' ';
span = span.Slice(8);
lastName.AsSpan().CopyTo(span);
return result;
}
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "FastAllocateString")]
static extern string FastAllocateString(string _, int length);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_firstChar")]
static extern ref char GetRawStringData(string _);
private static void Fill(IFormatProvider provider, Span<char> initialBuffer,
[InterpolatedStringHandlerArgument(nameof(provider), nameof(initialBuffer))]
ref DefaultInterpolatedStringHandler handler)
{
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment