はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】string.FormatとDefaultInterpolatedStringHandlerで処理速度・GC.Allocの差を計測してみる(string.Formatはボックス化の可能性あり)

はじめに

今回はstring.FormatDefaultInterpolatedStringHandlerで処理速度・Allocationの優劣を調べてみようと思います。

learn.microsoft.com

learn.microsoft.com

結論から言うとDefaultInterpolatedStringHandlerが優秀です。(C#10から補完文字列(interpolated string)でも利用されだしているので当然ですが)

string.Format

string.Formatの欠点は引数がobjectなことです。Stack上のメモリに確保されていた値はHeap上にコピーされてしまいます。(いわゆるボックス化)
ボックス化 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C

// 一部抜粋
public static string Format (string format, object? arg0);
public static string Format (string format, object? arg0, object? arg1, object? arg2);
public static string Format (string format, params object?[] args);

String.Format メソッド (System) | Microsoft Learn

BenchmarkDotNetというベンチマーク用ライブラリを用いて、処理速度・メモリ確保量を調べてみます。
github.com

private const int a = 10;
private const int b = 20;
private const int c = 30;
private const string x = "10";
private const string y = "20";
private const string z = "30";
        
[Benchmark]
public void StringFormatTest1()
{
    // Mean : 56.09 ns
    // GC.Alloc : 120 B
    // int -> object : ボックス化(Boxing allocation: conversion from 'int' to 'object' requires boxing of the value typ)
    _ = string.Format("{0}, {1}, {2}", a, b, c);
}

[Benchmark]
public void StringFormatTest2()
{
    // Mean : 52.10 ns
    // GC.Alloc : 48 B
    _ = string.Format("{0}, {1}, {2}", x, y, z);
}

intからobjectへの変換が行われたことによるボックス化で、単純にstringを扱う場合に比べてAllocationが多くなってしまっています。

DefaultInterpolatedStringHandler

C#10から登場したDefaultInterpolatedStringHandlerを利用して、同様の処理を実現してみたいと思います。
ufcpp.net

private const int a = 10;
private const int b = 20;
private const int c = 30;
private const string x = "10";
private const string y = "20";
private const string z = "30";

[Benchmark]
public void DefaultInterpolatedStringHandlerTest1()
{
    // Mean : 26.49 ns
    // GC.Alloc : 48 B
    // literalLength : 「{}」を除いた部分の文字列長
    // formattedCount : 「{}」の個数
    var handler = new DefaultInterpolatedStringHandler(4, 3);
    handler.AppendFormatted(a);
    handler.AppendLiteral(", ");
    handler.AppendFormatted(b);
    handler.AppendLiteral(", ");
    handler.AppendFormatted(c);

    _ = handler.ToStringAndClear();
}

[Benchmark]
public void DefaultInterpolatedStringHandlerTest2()
{
    // Mean : 21.92 ns
    // GC.Alloc : 48 B
    // literalLength : 「{}」を除いた部分の文字列長
    // formattedCount : 「{}」の個数
    var handler = new DefaultInterpolatedStringHandler(4, 3);
    handler.AppendFormatted(x);
    handler.AppendLiteral(", ");
    handler.AppendFormatted(y);
    handler.AppendLiteral(", ");
    handler.AppendFormatted(z);

    _ = handler.ToStringAndClear();
}

こちらはボックス化が働いていないため、同じAllocation量になっていました。

計測結果まとめ

処理速度・メモリ使用量ともに、DefaultInterpolatedStringHandlerが優れているという結果になりました。

Method Mean Error StdDev Allocated
StringFormatTest1(int) 56.09 ns 0.160 ns 0.141 ns 120 B
StringFormatTest2(string) 52.10 ns 0.286 ns 0.239 ns 48 B
DefaultInterpolatedStringHandlerTest1(int) 26.49 ns 0.302 ns 0.268 ns 48 B
DefaultInterpolatedStringHandlerTest2(string) 21.92 ns 0.085 ns 0.075 ns 48 B

計測で利用したコード

public class Program
{
    public static void Main(string[] args)
    {
        BenchmarkRunner.Run<TestClass>();
    }
    
    [MemoryDiagnoser(false)]
    public class TestClass
    {
        private const int a = 10;
        private const int b = 20;
        private const int c = 30;
        private const string x = "10";
        private const string y = "20";
        private const string z = "30";
        
        [Benchmark]
        public void StringFormatTest1()
        {
            // GC.Alloc : 120 B
            // int -> object : ボックス化(Boxing allocation: conversion from 'int' to 'object' requires boxing of the value typ)
            _ = string.Format("{0}, {1}, {2}", a, b, c);
        }

        [Benchmark]
        public void StringFormatTest2()
        {
            // GC.Alloc : 48 B
            _ = string.Format("{0}, {1}, {2}", x, y, z);
        }
    
        [Benchmark]
        public void DefaultInterpolatedStringHandlerTest1()
        {
            // GC.Alloc : 48 B
            // literalLength : 「{}」を除いた部分の文字列長
            // formattedCount : 「{}」の個数
            var handler = new DefaultInterpolatedStringHandler(4, 3);
            handler.AppendFormatted(a);
            handler.AppendLiteral(", ");
            handler.AppendFormatted(b);
            handler.AppendLiteral(", ");
            handler.AppendFormatted(c);

            _ = handler.ToStringAndClear();
        }

        [Benchmark]
        public void DefaultInterpolatedStringHandlerTest2()
        {
            // GC.Alloc : 48 B
            // literalLength : 「{}」を除いた部分の文字列長
            // formattedCount : 「{}」の個数
            var handler = new DefaultInterpolatedStringHandler(4, 3);
            handler.AppendFormatted(x);
            handler.AppendLiteral(", ");
            handler.AppendFormatted(y);
            handler.AppendLiteral(", ");
            handler.AppendFormatted(z);

            _ = handler.ToStringAndClear();
        }
    }
}