DefaultInterpolatedStringHandlerを.NET Framework向けに実装してみた記録
.NET 6.0(C# 10.0)と共に、Improved interpolated stringsが導入されます。
単独の補間文字列を使用した場合、DefaultInterpolatedStringHandler
へのシンタックスシュガーとなりますが、.NET 6.0より古い環境には当然存在しません。
古い環境(.NET Framework)でもDefaultInterpolatedStringHandler
を自前で用意して動かしてみた、という実験の記録です。
.NET Frameworkではなくても、.NET Standard 2.0でも使用できるかと思います。
6.0.100-preview.7.21379.14 での結果です。
DefaultInterpolatedStringHandler
.NET 6のDefaultInterpolatedStringHandler.csを<TargetFramework>net48</TargetFramework>
環境にそのまま移植しても、コンパイル出来ません。
同じく .NET 6.0と同時に公開されたISpanFormattable<T>
インターフェースが古い環境には存在せず、それらを実装したクラスもありません。(.NET Core系ならinternalで存在しますが。)
ISpanFormattable<T>
インターフェースを使用している箇所をコメントアウトするだけだとToString()
を使用してボックス化が多発するので、(恐らく)あまり性能が良くありません。
そのため、ISpanFormattable<T>
を使用することは諦めてプリミティブ型に対するAppend
があるStringBuilder
へ移譲することにします。
DateTime
等は目を瞑ります。
(本格的に対応するなら、Cysharp/ZStringを採用する方がいいでしょう。)
毎度StringBuilder
をnewしていると遅そうなので、TLSでインスタンスを保持することにしておきます。
※なお、Production環境で使用することを想定してません。
以下がソースコードです。
#nullable enable
using System.Text;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
namespace System.Runtime.CompilerServices
{
//[InterpolatedStringHandler]
internal partial struct DefaultInterpolatedStringHandler
{
private const int DefaultBufferSize = 256;
[ThreadStatic]
private static StringBuilder? t_buffer;
private static StringBuilder AcquireBuffer()
{
var buffer = t_buffer;
if (buffer == null)
{
return new StringBuilder(DefaultBufferSize);
}
else
{
buffer.Clear();
return buffer;
}
}
private readonly StringBuilder _buffer;
private readonly IFormatProvider? _provider;
private readonly ICustomFormatter? _formatter;
public DefaultInterpolatedStringHandler(int literalLength, int formattedCount)
{
_provider = null;
_buffer = AcquireBuffer();
_formatter = null;
}
public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider)
{
_provider = provider;
_buffer = AcquireBuffer();
_formatter = GetCustomFormatter(provider);
}
public override string ToString() => _buffer.ToString();
public string ToStringAndClear()
{
string result = _buffer.ToString();
if (_buffer.Capacity <= 65536)
{
t_buffer = _buffer;
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendLiteral(string value)
{
_buffer.Append(value);
}
public void AppendFormatted(object? value, string? format = null)
{
if (_formatter?.Format(format, value, _provider) is string customFormatted)
{
_buffer.Append(customFormatted);
return;
}
var result =
value is IFormattable formattable ?
formattable.ToString(format, _provider) :
(value?.ToString());
if (result is not null)
{
_buffer.Append(result);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(string? value)
{
if (value is not null)
{
if (_formatter is null)
{
_buffer.Append(value);
}
else if (_formatter?.Format(null, value, _provider) is string customFormatted)
{
_buffer.Append(customFormatted);
}
}
}
public void AppendFormatted(object? value, int alignment, string? format = null)
{
if (alignment is not 0) throw new NotImplementedException();
AppendFormatted(value, format);
}
private static ICustomFormatter? GetCustomFormatter(IFormatProvider? provider)
{
return provider is not null && provider.GetType() != typeof(CultureInfo) ?
provider.GetFormat(typeof(ICustomFormatter)) as ICustomFormatter : null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Boolean value) => _buffer.Append(value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.SByte value) => _buffer.Append(value);
public void AppendFormatted(System.SByte value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Byte value) => _buffer.Append(value);
public void AppendFormatted(System.Byte value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Char value) => _buffer.Append(value);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Int16 value) => _buffer.Append(value);
public void AppendFormatted(System.Int16 value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Int32 value) => _buffer.Append(value);
public void AppendFormatted(System.Int32 value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Int64 value) => _buffer.Append(value);
public void AppendFormatted(System.Int64 value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Single value) => _buffer.Append(value);
public void AppendFormatted(System.Single value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Double value) => _buffer.Append(value);
public void AppendFormatted(System.Double value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.Decimal value) => _buffer.Append(value);
public void AppendFormatted(System.Decimal value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.UInt16 value) => _buffer.Append(value);
public void AppendFormatted(System.UInt16 value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.UInt32 value) => _buffer.Append(value);
public void AppendFormatted(System.UInt32 value, string? format) => _buffer.Append(value.ToString(format));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendFormatted(System.UInt64 value) => _buffer.Append(value);
public void AppendFormatted(System.UInt64 value, string? format) => _buffer.Append(value.ToString(format));
}
}
ベンチマーク
下記の様に、コンパイルし直しただけで.NET Framework 4.8 環境で約27%高速化しました。
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 5 5600X, 1 CPU, 12 logical and 6 physical cores
[Host] : .NET Framework 4.8 (4.8.4400.0), X64 RyuJIT
.NET Framework 4.7.2 : .NET Framework 4.8 (4.8.4400.0), X64 RyuJIT
Job=.NET Framework 4.7.2 Runtime=.NET Framework 4.7.2
.NET SDK 6.0.100-preview.7.21379.14で コンパイルした場合
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
HelloWorld | 171.5 ns | 3.45 ns | 3.39 ns | 0.0262 | 168 B |
.NET SDK 5.0.400 でコンパイルした場合
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
HelloWorld | 234.9 ns | 1.83 ns | 1.62 ns | 0.0377 | 241 B |
ベンチマークコード
SDKの切り替えは、別途global.json
で行ってます。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(System.Reflection.Assembly.GetEntryAssembly()).Run(args);
internal class BenchmarkConfig : BenchmarkDotNet.Configs.ManualConfig
{
public BenchmarkConfig()
{
AddDiagnoser(MemoryDiagnoser.Default);
}
}
[SimpleJob(RuntimeMoniker.Net472)]
[Config(typeof(BenchmarkConfig))]
public class Program
{
private int a = 100, b = 200, c = 300;
[Benchmark]
public string HelloWorld() => $"Hello World!{a}.{b}.{c}";
}
Discussion
自己レス。
ZStrngでDefaultInterpolatedStringHandlerを実装したが、とても遅かった。