C#の文字列の扱いに関するよくある小ネタ2。(前回はインスタンス作成のお話でした)
人間が書いたコードにはコンパイル後に最適化がかかるため、出力されるIL(中間言語)は実際に書いたコードと異なりより良いパフォーマンスが出るように書き換えられます。
C#の文字列連結で基本的な+演算子についても同様に最適化がかかります。不変型について知ったばかりだと「+する度にstringインスタンスが作られるのでは?」と考えるかもしれませんが、実はそうなりません。
すでにこちらの記事にもう書かれているのですが、ここでは実際にコードを書いて逆アセンブルしてみます。
どんな最適化が行われるのかについては先に結論を。
結論
- リテラル文字列(固定値)同士の連結はコンパイル時に行われる
- +演算子による連結はString::Concat()に置き換えられる
逆アセンブラの場所
バージョンにもよりますが、
"C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\ildasm.exe"
などの場所にあります。
起動してウィンドウに見たいexeファイルを放り込みましょう。
リテラル文字列同士の連結
using System;
public class Program
{
public static void Main()
{
Console.WriteLine("hoge" + "fuga");
}
}
このコードがコンパイルされると次のようになります。
// 前略
IL_0001: ldstr "hogefuga"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
// 後略
この通り、すでに"hogefuga"になっていますね。
+演算子による連結
using System;
public class Program
{
public static void Main()
{
string a = "A", b = "B";
Console.WriteLine(a + b);
}
}
このコードがコンパイルされると次のようになります。
// 前略
IL_0001: ldstr "A"
IL_0006: stloc.0
IL_0007: ldstr "B"
IL_000c: stloc.1
IL_000d: ldloc.0
IL_000e: ldloc.1
IL_000f: call string [mscorlib]System.String::Concat(string,
string)
IL_0014: call void [mscorlib]System.Console::WriteLine(string)
// 後略
+演算子による連結処理が[mscorlib]System.String::Concat(string,string)です。
これはstring.Concat(string,string)関数と同じもので、実際にstring.Concat(a,b)と書いても全く同じILコードが出ます。
それでは3つの文字列連結はどうなるのでしょうか?
string.Concatが二回呼ばれるのでしょうか? さてどうでしょう。もう前後は省略してきますが…。
string a = "A", b = "B", c = "C";
Console.WriteLine(a + b + c);
IL_0001: ldstr "A"
IL_0006: stloc.0
IL_0007: ldstr "B"
IL_000c: stloc.1
IL_000d: ldstr "C"
IL_0012: stloc.2
IL_0013: ldloc.0
IL_0014: ldloc.1
IL_0015: ldloc.2
IL_0016: call string [mscorlib]System.String::Concat(string,
string,
string)
IL_001b: call void [mscorlib]System.Console::WriteLine(string)
このように一回のString::Concat()で連結してくれるのでした。
では4つ連結してみるとどうなるのでしょう?
string a = "A", b = "B", c = "C", d = "D";
Console.WriteLine(a + b + c + d);
IL_0001: ldstr "A"
IL_0006: stloc.0
IL_0007: ldstr "B"
IL_000c: stloc.1
IL_000d: ldstr "C"
IL_0012: stloc.2
IL_0013: ldstr "D"
IL_0018: stloc.3
IL_0019: ldloc.0
IL_001a: ldloc.1
IL_001b: ldloc.2
IL_001c: ldloc.3
IL_001d: call string [mscorlib]System.String::Concat(string,
string,
string,
string)
IL_0022: call void [mscorlib]System.Console::WriteLine(string)
4つだって一度でやってくれました。
では5つなら果たして…?
string a = "A", b = "B", c = "C", d = "D", e = "E";
Console.WriteLine(a + b + c + d + e);
IL_0001: ldstr "A"
IL_0006: stloc.0
IL_0007: ldstr "B"
IL_000c: stloc.1
IL_000d: ldstr "C"
IL_0012: stloc.2
IL_0013: ldstr "D"
IL_0018: stloc.3
IL_0019: ldstr "E"
IL_001e: stloc.s e
IL_0020: ldc.i4.5
IL_0021: newarr [mscorlib]System.String
IL_0026: stloc.s CS$0$0000
IL_0028: ldloc.s CS$0$0000
IL_002a: ldc.i4.0
IL_002b: ldloc.0
IL_002c: stelem.ref
IL_002d: ldloc.s CS$0$0000
IL_002f: ldc.i4.1
IL_0030: ldloc.1
IL_0031: stelem.ref
IL_0032: ldloc.s CS$0$0000
IL_0034: ldc.i4.2
IL_0035: ldloc.2
IL_0036: stelem.ref
IL_0037: ldloc.s CS$0$0000
IL_0039: ldc.i4.3
IL_003a: ldloc.3
IL_003b: stelem.ref
IL_003c: ldloc.s CS$0$0000
IL_003e: ldc.i4.4
IL_003f: ldloc.s e
IL_0041: stelem.ref
IL_0042: ldloc.s CS$0$0000
IL_0044: call string [mscorlib]System.String::Concat(string[])
IL_0049: call void [mscorlib]System.Console::WriteLine(string)
String::Concat(string[])になりました。内部で勝手に配列化しているわけですね。ここまで来ても一度のConcatで済ませてくれるので、stringインスタンスが毎回作られるようなことはないということがわかると思います。
ちなみにstring.Concat(a, b, c, d, e)と書いても全く同じILコードが出ます。+演算子の文字列連結はコンパイラがConcatに翻訳してくれているんですね。
おまけ
前回の空文字列とstring.Emptyのお話から、両者は同じ参照を持つことがわかりましたがILコードはどうなっているのでしょうか
string a = "", b = string.Empty;
Console.WriteLine(a + b);
IL_0001: ldstr ""
IL_0006: stloc.0
IL_0007: ldsfld string [mscorlib]System.String::Empty
IL_000c: stloc.1
IL_000d: ldloc.0
IL_000e: ldloc.1
IL_000f: call string [mscorlib]System.String::Concat(string,
string)
IL_0014: call void [mscorlib]System.Console::WriteLine(string)
ここで見る限り、string.EmptyはStringクラスのEmptyプロパティを取って来ています。
参照が同じになる、というのもこれだけ見てもわかりません。それは実行時の最適化や振る舞いによる結果なんですね。