C#における最速の文字列結合を検証する

C#を使用する時に文字列を結合する時は多々あるが、主に用いられるのは`+演算子`を用いた結合と`StringBuilder`を用いた結合である。

```c#
string str = "";
System.Text.StringBuilder Sb = new System.Text.StringBuilder ();

//+演算子による結合
str += "hoge";

//StringBuilderによる結合
Sb.Append ("huga");
str = Sb.ToString ();
```

この2つの結合方法でしばしば比較されるのが処理速度だろう。
1回結合するだけなら+演算子の方が高速、StringBuilderの方が圧倒的に速いという話がある。だが、前者の納得の行く検証は見つからず、後者も少ない結合回数での検証は見つからなかった。なので今回はどちらが速いのか検証する事にした。

#検証用のコード
以下のコードを使用した。

```c#
using System;

class Class
{
	static void Main ()
	{
		string str = "";//こいつに文字を追加する
		System.Text.StringBuilder Sb = new System.Text.StringBuilder ();//こいつに文字を追加する
		string s1 = "あたいったら最強ね！";//追加する文字
		string s2 = "あたいったら";
		string s3 = "最強ね！";
		int n;//文字を加算する回数
		int i;
		int j;
		int k;
		System.Diagnostics.Stopwatch Sw = new System.Diagnostics.Stopwatch ();//処理時間計測用ストップウォッチ
		double TotalTime = 0;

		for (k = 0; k < 13; k++)
		{
			//加算回数を1～10、100、1000、10000にする
			if (k < 9)
			{
				n = k + 1;
			}
			else
			{
				n = (int)Math.Pow (10, k - 8);
			}
			//加算回数を表示
			Console.WriteLine ("n=" + n.ToString ());

			//1：+= s1
			for (j = 0; j < 10; j++)
			{
				str = "";//文字列とストップウォッチをリセット
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					str += s1;
				}
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Console.WriteLine ("+= s1：" + (TotalTime / 10) + "ms");

			//2：Append (s1)
			TotalTime = 0;
			for (j = 0; j < 10; j++)
			{
				str = "";
				Sb.Clear ();
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					Sb.Append (s1);
				}
				str = Sb.ToString ();
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Console.WriteLine ("Append (s1)：" + (TotalTime / 10) + "ms");

			//3：+= s2 + s3
			TotalTime = 0;
			for (j = 0; j < 10; j++)
			{
				str = "";
				Sb.Clear ();
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					str += s2 + s3;
				}
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Console.WriteLine ("+= s2 + s3：" + (TotalTime / 10) + "ms");

			//4：Append (s2).Append (s3)
			TotalTime = 0;
			for (j = 0; j < 10; j++)
			{
				str = "";
				Sb.Clear ();
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					Sb.Append (s2).Append (s3);
				}
				str = Sb.ToString ();
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Console.WriteLine ("Append (s2).Append (s3)：" + (TotalTime / 10) + "ms");

			//5：Append (s2 + s3)
			TotalTime = 0;
			for (j = 0; j < 10; j++)
			{
				str = "";
				Sb.Clear ();
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					Sb.Append (s2 + s3);
				}
				str = Sb.ToString ();
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Console.WriteLine ("Append (s2 + s3)：" + (TotalTime / 10) + "ms");
			//見やすくするために改行
			Console.WriteLine ();
		}

		//いずれかのキーを押してコンソールを終了する
		Console.WriteLine ("Press any key to exit.");
		Console.ReadKey ();
	}
}
```

同じ処理をしている場所が多い?変数増やしたくなかったからしょうがない。
それは置いといて実際に動かすと以下のようなコンソールが表示される。(Visual Studioの場合)
![実行した場合](https://qiita-image-store.s3.amazonaws.com/0/318712/5e3202b3-79bd-b9aa-1f24-49ea1c79351f.png)
検証の概要は以下のようになる。

- string型のstrに文字をn回追加する時間、またはStringBuilderクラスのSbに文字をn回追加してToString()を行う時間を10回計測し、平均の処理時間を求める
- 追加する文字列をstring型のs1、s2、s3の3種類とする(s1 = s2 + s3)
- n=1,2,3,4,5,6,7,8,9,10,100,1000,10000の13通り
- 以下の5種類の処理について処理時間を計測する(Sbを使っている場合はstr = Sb.ToString ();の時間も含める)
    - 1：str += s1;
    - 2：Sb.Append (s1);
    - 3：str += s2 + s3;
    - 4：Sb.Append (s2).Append (s3);
    - 5：Sb.Append (s2 + s3);

上の5種類の処理では最終的にstrの値は同一になる。~~あたいったら最強ね！~~

#検証結果
上のコードを実行し、処理時間をまとめた表を以下に示す。

|n\実行処理|str += s1;|Sb.Append (s1);|str += s2 + s3;|Sb.Append (s2).Append (s3);|Sb.Append (s2 + s3);|
|:------------:|:------------|:------------|:------------|:------------|:------------|
|n=1|0.00054ms|0.00036ms|0.00028ms|0.00032ms|0.00028ms|
|n=2|0.0006ms|0.00062ms|0.00096ms|0.00036ms|0.00056ms|
|n=3|0.001ms|0.00044ms|0.00056ms|0.00104ms|0.00048ms|
|n=4|0.00096ms|0.00044ms|0.00132ms|0.00056ms|0.00594ms|
|n=5|0.00655ms|0.0045ms|0.00432ms|0.00069ms|0.00345ms|
|n=6|0.00422ms|0.00061ms|0.00273ms|0.00065ms|0.00273ms|
|n=7|0.00422ms|0.00069ms|0.00162ms|0.00065ms|0.00105ms|
|n=8|0.00531ms|0.00133ms|0.00161ms|0.00057ms|0.00221ms|
|n=9|0.00386ms|0.00084ms|0.00245ms|0.00133ms|0.00109ms|
|n=10|0.0033ms|0.00072ms|0.00237ms|0.00085ms|0.00242ms|
|n=100|0.13122ms|0.00508ms|0.1108ms|0.00545ms|0.01035ms|
|n=1000|2.35291ms|0.03973ms|2.46658ms|0.05636ms|0.12499ms|
|n=10000|206.69497ms|0.27328ms|184.16597ms|0.3683ms|0.61013ms|

<font color = "red">**StringBuilderはやっぱり速かった**</font>
1万回文字を追加しても1msも掛からないのは流石だ。一度に追加するstringを2個に増やしてもあまり遅くならないのは魅力的だ。
+演算子による加算はnが大きくなるにつれて処理時間が増えている。だが、nが2以下ではStringBuilderと処理時間は大差ないようだ。
また、一度に加算するstringを2個に増やしても処理時間が増えないのは驚いた。むしろ減っているまである。

Sb.Append (s2 + s3);は見る人から見ればクソコードだが誰も検証してないのでこの際だから検証した。
Sb.Append (s2).Append (s3);の倍以上時間が掛かっていてやはりクソだった。~~nが3と4の時は誤差でしょ~~

#まとめ
<font color = "red">**文字列結合はStringBuilderを使おう!**</font>

- +演算子による文字列結合とStringBuilderによる文字列結合ではStringBuilderの方が圧倒的に高速
- ただし、文字の結合回数が2回以下ではどちらも大差無い
- +演算子による文字列結合では、一度に追加するstringの個数が1個でも2個でも処理時間は変わらない(むしろ後者の方が速いかも)
- StringBuilderによる文字列結合では、一度に追加するstringの個数が1個でも2個でも処理時間は少し遅くなる程度で済む
- ただし、Sb.Append (hoge + huga)はSb.Append (hoge).Append (huga)に比べて遥かに遅いので、前者はやめて後者を使おう!

追記
一度に結合する文字列を増やす時、場合によっては+演算子による結合の方がStringBuilderによる結合よりも速くなるという~~マサカリ~~コメントを頂いた。
これについても検証したので気になる人はこちらも参照→[C#で一度に複数の文字列を結合した時の処理時間を計測する](https://qiita.com/TD12734/items/8799b2f6f7b8c257c3ee)
結論から言うと<font color = "red">**多くの場合においてStringBuilderによる結合の方が処理が速い**。</font>
+演算子が速くなるのは結合する文字列に定数しかない、または定数ばかりで結合する数が多い時ぐらいだった。

#おまけ：Unity環境での文字列結合速度の計測
Unity環境でも上のようなコードを作って文字列結合の速度を計測した。
結論を言うとこの記事で検証した結果とほぼ同じ結果になった。違いを言うなればn=10000で一気に処理時間が長くなった事ぐらいか。

<details><summary>詳しい検証と結果は折り畳みの中にある</summary><div>
まず、計測用のコードは以下の通り。

```c#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Test : MonoBehaviour
{
	void Awake ()
	{
		string str = "";//こいつに文字を追加する
		System.Text.StringBuilder Sb = new System.Text.StringBuilder ();//こいつに文字を追加する
		string s1 = "あたいったら最強ね！";//追加する文字
		string s2 = "あたいったら";
		string s3 = "最強ね！";
		int n;//文字を加算する回数
		int i;
		int j;
		int k;
		System.Diagnostics.Stopwatch Sw = new System.Diagnostics.Stopwatch ();//処理時間計測用ストップウォッチ
		double TotalTime = 0;

		for (k = 0; k < 13; k++)
		{
			//加算回数を1～10、100、1000、10000にする
			if (k < 9)
			{
				n = k + 1;
			}
			else
			{
				n = (int)Mathf.Pow (10, k - 8);
			}
			//加算回数を表示
			Debug.Log ("n=" + n.ToString ());

			//1：+= s1
			for (j = 0; j < 10; j++)
			{
				str = "";//文字列とストップウォッチをリセット
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					str += s1;
				}
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Debug.Log ("+= s1：" + (TotalTime / 10) + "ms");

			//2：Append (s1)
			TotalTime = 0;
			for (j = 0; j < 10; j++)
			{
				str = "";
				Sb.Clear ();
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					Sb.Append (s1);
				}
				str = Sb.ToString ();
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Debug.Log ("Append (s1)：" + (TotalTime / 10) + "ms");

			//3：+= s2 + s3
			TotalTime = 0;
			for (j = 0; j < 10; j++)
			{
				str = "";
				Sb.Clear ();
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					str += s2 + s3;
				}
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Debug.Log ("+= s2 + s3：" + (TotalTime / 10) + "ms");

			//4：Append (s2).Append (s3)
			TotalTime = 0;
			for (j = 0; j < 10; j++)
			{
				str = "";
				Sb.Clear ();
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					Sb.Append (s2).Append (s3);
				}
				str = Sb.ToString ();
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Debug.Log ("Append (s2).Append (s3)：" + (TotalTime / 10) + "ms");

			//5：Append (s2 + s3)
			TotalTime = 0;
			for (j = 0; j < 10; j++)
			{
				str = "";
				Sb.Clear ();
				Sw.Reset ();
				Sw.Start ();
				for (i = 0; i < n; i++)
				{
					Sb.Append (s2 + s3);
				}
				str = Sb.ToString ();
				Sw.Stop ();
				TotalTime += Sw.Elapsed.TotalMilliseconds;
			}
			//かかった時間を表示
			Debug.Log ("Append (s2 + s3)：" + (TotalTime / 10) + "ms");
		}
	}
}
```

見ての通り上のコードとほぼ同じである。出力先を変えただけ。
これを適当なオブジェクトにアタッチして実行したらコンソールログがこんな感じになる。
![Unity1](https://qiita-image-store.s3.amazonaws.com/0/318712/eeef50b1-3192-183f-7c58-8672e90b5363.png) ![Unity2](https://qiita-image-store.s3.amazonaws.com/0/318712/b4dc14d0-6dbd-db5b-188b-cbce36f56ad9.png)

結果は上での検証結果と同じような感じだったので特に表にはまとめない。ただ、上の右の画像のようにn=10000で一気に遅くなった。
</div></details>