PG日誌

各記事はブラウザの横幅を1410px以上にすると2カラムの見出しが表示されます。なるべく横に広げてみてください。

C#の文字列補完はToStringした方が高速になる理由を調べてみた

はじめに

C# の「文字列補完」機能という機能があります。ものすごい雑に言うと C# 6 以降で可能な文字列の先頭に「$」起動を付けることでフォーマット付き文字列の視認性を向上することができる機能です。例えば以下のような記述ができます。

// (1)
// 旧来の記述方法
int a = 10;
// + で連結する
string msg1 = "(" + a + ")";
// もしくは string.Format メソッドを使う
string msg2 = string.Format("({0})", a);
> (10)

// (2)
// 文字列補完を使った表現
string msg3 = $"({a})";
> (10)

string.Format をインラインで記述できるようになったイメージです。

確認環境

  • .NET Core3.1
  • VisualStudio2019
  • Windows10

{ } の中はToString した方が高速?

まずは確認ですが記述方法によって速度にが明確に差があります。

これがどういうことから調査したいと思います。以下のコードでは「case.3」が高速です。「全部 ToString した時だけ」動作が早いです。

int a = 10;
int b = 20;

// case.1
string msg1 = $"{a}, {b}";
// > 0.000258828msec

// case.2
string msg2 = $"{a}, {b.ToString()}";
// > 0.000250034msec

// case.3
string msg3 = $"{a.ToString()}, {b.ToString()}";
// > 0.000158491msec

約40%も速度が違うため実行資源が限られている環境(≒モバイル)では大半のケースで ToString するのがベストプラクティスだと思います。

どうして速度が違うのか?

では、どうしてこのような違いが出たのでしょうか?こういう時はSharpLabが便利です。

先述のコードをツールにかけると概ね以下のようになります。

// case.1
string msg1 = $"{a}, {b}";
↓
string msg1 = string.Format("{0}, {1}", a, b);

// case.2
string msg2 = $"{a}, {b.ToString()}";
↓
string msg2 = string.Format("{0}, {1}", a, b.ToString());

// case.3
string msg3 = $"{a.ToString()}, {b.ToString()}";
↓
string msg3 = string.Concat(a.ToString(), ", ", b.ToString());

速度の違いはコンパイラがコードをILにした時に違いがあるからのようです。展開のされ方が違います。

全部文字列だと判断された場合だけ「string.Format」ではなく「string.Concat」が使用されています。string.Format の方が実装が複雑なので処理時間が違うようです(この程度だとボックス化による処理速度の影響はほぼ無視できる程度のため純粋に使用したメソッドの性能によるところが大きいと思います)

つまり、全部文字列だったら早い ので、一部を ToString するだけでは効果がありません。

IDE0071 保管を簡略化することができますが煩わしい場合

VisualStudio2019 では文字列補完中に ToString を書くと「IDE0071 保管を簡略化することができます」と警告が表示されます。

ただ、上記の結果から明示して使用するケースがあるため煩わしい場合は、以下をコードの先頭に記述すればこの警告が抑制できます。

// コードの先頭 ~ using 後くらいの位置に以下を記述する
#pragma warning disable IDE0071

検証コード

この検証で使用したコードは以下の通りです。

using System;
using System.Diagnostics;

#pragma warning disable IDE0071, IDE0059

internal class AppMain
{
    public static void Main(string[] args)
    {
        int a = 10;
        int b = 20;

        var ast = new Stopwatch();
        var bst = new Stopwatch();
        var cst = new Stopwatch();

        string msg1 = "";
        string msg2 = "";
        string msg3 = "";

        int cnt = 100000;

        for (int i = 0; i < cnt; i++)
        {
            ast.Start();
            msg1 = $"{a}, {b}";
            ast.Stop();

            bst.Start();
            msg2 = $"{a}, {b.ToString()}";
            bst.Stop();

            cst.Start();
            msg3 = $"{a.ToString()}, {b.ToString()}";
            cst.Stop();
        }

        Console.WriteLine($"{ast.Elapsed.TotalMilliseconds / cnt}msec");
        Console.WriteLine($"{bst.Elapsed.TotalMilliseconds / cnt}msec");
        Console.WriteLine($"{cst.Elapsed.TotalMilliseconds / cnt}msec");
    }
}