Created
August 14, 2023 11:57
-
-
Save davepcallan/64f3153efbd06fa7cfd84bc11a7d60d4 to your computer and use it in GitHub Desktop.
Performance Sensitive simple string concatenation in .NET 8
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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