10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ISpanFormattableを使おう

Last updated at Posted at 2023-12-12

はじめに

この記事はC# Advent Calendar 2023の13日目の記事です。
12日目の記事は @aneuf による【C#】DefaultInterpolatedStringHandler を StringBuilder 的に使うでした。

ISpanFormattableとは

ISpanFormattableとはString Interpolationで呼ばれるノーアロケーションな文字列生成インターフェースです。
このインターフェースを実装していない場合はstring ToString()が呼ばれてstringクラスのインスタンスがアロケートされてしまうのですが、そのアロケーションがない分高速に動作します。

残念なことにStringBuilderでは使うことができないようなのですが、それでも依然として強力な機能です。
(AppendSpanFormattableというものはinternal状態で存在するのでUnsafeAccessorあたりを使えば呼べなくはなさそうですがオフィシャルには使用不可能です)

12日目の記事中にもISpanFormattableについての記載がありますので合わせてご確認ください!
https://aneuf.hatenablog.com/entry/2023/12/12/000000#%E5%80%A4%E3%81%AE%E6%9B%B8%E3%81%8D%E8%BE%BC%E3%81%BF--ISpanFormattable

使い方

ISpanFormattableを実装するだけで動作します。

使い道

ログの出力など繰り返し呼ばれるような箇所で利用することで積もり積もったアロケーションコストを削減することができます。

具体例

以下のようなクラスを例に挙げてISpanFormattableに対応してみます。

record class KVP(int Key, int Value)
{
    public override string ToString() => $"{Key:000000} : {Value}";
}

以下のような呼び出しを行った際にはToStringが呼ばれていることがわかります。

var kvp = new KVP(101, 10);
Console.WriteLine($"{kvp}"); // 000101 : 10

これをISpanFormattableに対応すると以下のようになります。

record class KVP(int Key, int Value) : ISpanFormattable
{
    // これはなくてもよい
    // public override string ToString() => $"{Key:000000} : {Value}";
    
    public string ToString(string? format, IFormatProvider? formatProvider)
    {
        var buffer = (stackalloc char[128]);
        if (!TryFormat(buffer, out var charsWritten, [], formatProvider)) return "";
        return new(buffer[..charsWritten]);
    }

    public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
    {
        charsWritten = 0;
        int cw;

        if (!Key.TryFormat(destination, out cw, "000000", provider)) return false;
        destination = destination[cw..];
        charsWritten += cw;

        if (destination.Length < 3) return false;
        destination[0] = ' ';
        destination[1] = ':';
        destination[2] = ' ';
        destination = destination[3..];
        charsWritten += 3;

        if (!Value.TryFormat(destination, out cw, format, provider)) return false;
        //destination = destination[cw..];
        charsWritten += cw;
        
        return true;
    }
}

これを先ほどと同じように呼び出すと同じ結果になることがわかります。

var kvp = new KVP(101, 10);
Console.WriteLine($"{kvp}"); // 000101 : 10

計測

手元でベンチマークを取ってみたところ以下のようにAllocatedが2倍ほど違う状態でした。

BenchmarkDotNet v0.13.11, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2)
11th Gen Intel Core i7-11700 2.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.100
  [Host]     : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI


| Method             | Mean     | Error   | StdDev  | Gen0       | Allocated |
|------------------- |---------:|--------:|--------:|-----------:|----------:|
| KVPString          | 178.0 ms | 3.51 ms | 4.04 ms | 17333.3333 |  140.9 MB |
| KVPSpanFormattable | 113.2 ms | 1.61 ms | 1.43 ms |  9200.0000 |  74.26 MB |

検証コード : https://gist.github.com/the9ball/5165c63aff3aa9ff23ae66e9946f8f91

Note
上記ベンチマークではISpanFormattableの効果に絞って計測するためKVPをclassではなくstructにして使用しています。

まとめ

ISpanFormattableを積極的に使ってノーアロケーションな世界を作りましょう!

参考リンク

追記

どうも丁寧に書き込まなくてもdestination.TryWrite($"{Key:000000} : {Value}", out charsWritten)とするだけでスタックを使ってくれるのでアロケーションが少ないみたいです。

BenchmarkDotNet v0.13.11, Windows 11 (10.0.22621.2861/22H2/2022Update/SunValley2)
11th Gen Intel Core i7-11700 2.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.100
  [Host]     : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI


| Method              | Mean     | Error   | StdDev  | Gen0       | Allocated |
|-------------------- |---------:|--------:|--------:|-----------:|----------:|
| KVPString           | 184.4 ms | 3.61 ms | 5.93 ms | 17333.3333 | 140.89 MB |
| KVPSpanFormattable  | 127.3 ms | 2.49 ms | 3.33 ms |  9200.0000 |  74.27 MB |
| KVPSpanFormattable2 | 128.5 ms | 2.55 ms | 3.32 ms |  9200.0000 |  74.26 MB |

検証コード : https://gist.github.com/the9ball/24dcb9cb5036c059ec39bf249d87f205

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?