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

はじめに

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

記述例は以下の通りです。

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

// 上記を今までの方法で記述すると以下の通り
// (1) + で連結する
string msg1 = "(" + a + ")";
// > (1010)

// (2) Fromatメソッドを使う
string msg2 = string.Format("({0})", 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());

どうも展開のされ方が違うようです。

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

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

まぁもともと、string.Concat は文字列操作界隈では動作速度は最遅レベルですが、ワーストワンので番速度が遅い string.Format が使われるりは多少マシって感じです。まぁ、ここら辺の速度を Unity 上で追及するなら StringBuilder をインスタンスを使いまわしすか、ZString というライブラリを使ったほうがいいとは思いますが。

C#10.0では高速化されます

ちなみにこの文字列補完ですが、動作速度がいくら何でも遅すぎるということで、C#10.0から速度が大幅に向上します。

どうも展開のされ方が string.Fromat をから DefaultInterpolatedStringHandler という StringBuilder の親戚で処理されるようになって処理速度が大幅に改善したようです。なのでこの記事は .NET 6.0(C# 9.0) までの話になります。

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");
    }
}