.NET Frameworkには文字列を表現するクラスとしてStringクラスがあります。Stringクラスを使うと、文字列の連結や置換、挿入などの処理を簡単に行うことができます。
Stringオブジェクトには、その内容を変更することができないという特徴があります。Stringオブジェクトを連結、置換、挿入する場合も実は、その度に新しいStringオブジェクトが作成されています。
これに対して内容を変更できる文字列を表現したクラスが、StringBuilderクラス(System.Text名前空間)です。StringBuilderオブジェクトに対して文字列の追加、置換、挿入を行うと、そのオブジェクトの内容が変更されるだけで、新しいオブジェクトを作成しません。そのため、同じ文字列に対してこれらの処理を複数回行う場合は、Stringクラスの代わりにStringBuilderクラスを使うことでパフォーマンスが向上するケースが多いです。特にループ内ではStringBuilderを使用するのが一般的です。
なおパフォーマンスについてより詳しくは、後述します。
次の例では、String、StringBuilderクラスのそれぞれを使って文字列を何回も追加し、処理にかかった時間を計測しています。
Dim s As String = "0" 'String型を使って文字列を追加していく Dim t1 As Integer = System.Environment.TickCount Dim str1 As String = "" For i As Integer = 0 To 200000 str1 += s Next t1 = System.Environment.TickCount - t1 'かかった時間を表示 Console.WriteLine("String: {0}ミリ秒", t1) 'StringBuilderクラスを使って文字列を追加していく Dim t2 As Integer = System.Environment.TickCount Dim sb As New System.Text.StringBuilder() For i As Integer = 0 To 200000 sb.Append(s) Next Dim str2 As String = sb.ToString() t2 = System.Environment.TickCount - t2 'かかった時間を表示 Console.WriteLine("StringBuilder: {0}ミリ秒", t2) '結果例 'String: 24165ミリ秒 'StringBuilder: 16ミリ秒
string s = "0"; //String型を使って文字列を追加していく int t1 = System.Environment.TickCount; string str1 = ""; for (int i = 0; i < 200000; i++) { str1 += s; } t1 = System.Environment.TickCount - t1; //かかった時間を表示 Console.WriteLine("String: {0}ミリ秒", t1); //StringBuilderクラスを使って文字列を追加していく int t2 = System.Environment.TickCount; System.Text.StringBuilder sb = new System.Text.StringBuilder(); for (int i = 0; i < 200000; i++) { sb.Append(s); } string str2 = sb.ToString(); t2 = System.Environment.TickCount - t2; //かかった時間を表示 Console.WriteLine("StringBuilder: {0}ミリ秒", t2); //結果例 //String: 24165ミリ秒 //StringBuilder: 16ミリ秒
ここからは、StringBuilderのパフォーマンスについてより詳しく説明します。なお以下の説明では特に文字列を追加するケースについて取り上げていますが、文字列の置換や挿入などでもほぼ同様だと思ってください。
StringBuilder.Appendで文字列を追加していく時、最終的にどの程度の長さになるか分かっているならば、その長さをStringBuilder.Capacityプロパティにあらかじめ指定しておくとパフォーマンスが向上します。
Capacityプロパティは、StringBuilderのバッファの容量を表します。StringBuilderに追加された文字列がこの容量を超えた時は、自動的により大きなバッファを割り当て、そこに今までのデータをコピーします。よって、Capacityの値が頻繁に更新されると効率が落ちるため、始めから必要なだけ容量を確保しておきます。
補足:Capacityの値は、必要に応じて倍々に増えていきます。
Capacityプロパティは、StringBuilderのコンストラクタにパラメータで指定することができます。Capacityを指定していなかった時は、16というとても小さい値になってしまいます。
Capacityを指定した時としなかった時でどの程度の差が出るかを、以下のようなコードで試してみました。ビックリするほどの差は出ませんでしたが、確かにCapacityを指定した方が速かったです。
Dim s As String = "0" Dim loopCount As Integer = 100000000 'StringBuilderのCapacityを指定せずに文字列を追加していく Dim sw1 As New System.Diagnostics.Stopwatch() sw1.Start() Dim sb1 As New System.Text.StringBuilder() For i As Integer = 0 To loopCount sb1.Append(s) Next Dim str1 As String = sb1.ToString() sw1.Stop() 'かかった時間を表示 Console.WriteLine("Capacity指定なし: {0}", sw1.ElapsedTicks) 'StringBuilderのCapacityを指定して文字列を追加していく Dim sw2 As New System.Diagnostics.Stopwatch() sw2.Start() Dim sb2 As New System.Text.StringBuilder(s.Length * loopCount) For i As Integer = 0 To loopCount sb2.Append(s) Next Dim str2 As String = sb2.ToString() sw2.Stop() 'かかった時間を表示 Console.WriteLine("Capacity指定あり: {0}", sw2.ElapsedTicks) '結果例 'Capacity指定なし: 6480596 'Capacity指定あり: 5795984
string s = "0"; int loopCount = 100000000; //StringBuilderのCapacityを指定せずに文字列を追加していく System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch(); sw1.Start(); System.Text.StringBuilder sb1 = new System.Text.StringBuilder(); for (int i = 0; i < loopCount; i++) { sb1.Append(s); } string str1 = sb1.ToString(); sw1.Stop(); //かかった時間を表示 Console.WriteLine("Capacity指定なし: {0}", sw1.ElapsedTicks); //StringBuilderのCapacityを指定して文字列を追加していく System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch(); sw2.Start(); System.Text.StringBuilder sb2 = new System.Text.StringBuilder(s.Length * loopCount); for (int i = 0; i < loopCount; i++) { sb2.Append(s); } string str2 = sb2.ToString(); sw2.Stop(); //かかった時間を表示 Console.WriteLine("Capacity指定あり: {0}", sw2.ElapsedTicks); //結果例 //Capacity指定なし: 6480596 //Capacity指定あり: 5795984
「文字列を連結する」では、複数の文字列を連結する時、連結演算子(または、String.Concatメソッド)で一度に連結した方が、StringBuilder.Appendメソッドで連結するよりも高速であることを紹介しました。しかし、例えばループなどによって文字列を1つずつ連結していく場合は、連結する文字列が増えるほど連結演算子よりStringBuilder.Appendメソッドの方が速くなります。よって、StringBuilder.Appendメソッドはループ内で使用するというのが基本です。
具体的に、一体何回連結を繰り返せば連結演算子よりもStringBuilder.Appendメソッドの方が速くなるかを以下のようなコードで試してみました。
Dim s1 As New String("あ"c, 10) Dim appendCount As Integer = 0 While True appendCount += 1 '連結演算子で文字列を追加していく Dim sw1 As New System.Diagnostics.Stopwatch() sw1.Start() For i As Integer = 0 To 1000000 Dim r1 As String = "" For ai As Integer = 0 To appendCount - 1 r1 += s1 Next Next sw1.Stop() 'StringBuilderで文字列を追加していく Dim sw3 As New System.Diagnostics.Stopwatch() sw3.Start() For i As Integer = 0 To 1000000 Dim sb As New System.Text.StringBuilder() For ai As Integer = 0 To appendCount - 1 sb.Append(s1) Next Dim r3 As String = sb.ToString() Next sw3.Stop() 'StringBuilderの方が速くなったか調べる If sw1.ElapsedTicks > sw3.ElapsedTicks Then Console.WriteLine(appendCount) Exit While End If End While
string s1 = new string('あ', 10); int appendCount = 0; while (true) { appendCount++; //連結演算子で文字列を追加していく System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch(); sw1.Start(); for (int i = 0; i < 1000000; i++) { string r1 = ""; for (int ai = 0; ai < appendCount; ai++) { r1 += s1; } } sw1.Stop(); //StringBuilderで文字列を追加していく System.Diagnostics.Stopwatch sw3 = new System.Diagnostics.Stopwatch(); sw3.Start(); for (int i = 0; i < 1000000; i++) { System.Text.StringBuilder sb = new System.Text.StringBuilder(); for (int ai = 0; ai < appendCount; ai++) { sb.Append(s1); } string r3 = sb.ToString(); } sw3.Stop(); //StringBuilderの方が速くなったか調べる if (sw1.ElapsedTicks > sw3.ElapsedTicks) { Console.WriteLine(appendCount); break; } }
上記のコードで私が試したところ(.NET Framework 4.0)、大体8~9回でStringBuilder.Appendメソッドの方が速くなりました。
次に、StringBuilderのCapacityを指定した時と比べてみました。
Dim s1 As New String("あ"c, 10) Dim appendCount As Integer = 0 While True appendCount += 1 '連結演算子で文字列を追加していく Dim sw1 As New System.Diagnostics.Stopwatch() sw1.Start() For i As Integer = 0 To 1000000 Dim r1 As String = "" For ai As Integer = 0 To appendCount - 1 r1 += s1 Next Next sw1.Stop() 'StringBuilderで文字列を追加していく Dim sw3 As New System.Diagnostics.Stopwatch() sw3.Start() For i As Integer = 0 To 1000000 'Capacityを指定する Dim sb As New System.Text.StringBuilder(s1.Length * appendCount) For ai As Integer = 0 To appendCount - 1 sb.Append(s1) Next Dim r3 As String = sb.ToString() Next sw3.Stop() 'StringBuilderの方が速くなったか調べる If sw1.ElapsedTicks > sw3.ElapsedTicks Then Console.WriteLine(appendCount) Exit While End If End While
string s1 = new string('あ', 10); int appendCount = 0; while (true) { appendCount++; //連結演算子で文字列を追加していく System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch(); sw1.Start(); for (int i = 0; i < 1000000; i++) { string r1 = ""; for (int ai = 0; ai < appendCount; ai++) { r1 += s1; } } sw1.Stop(); //StringBuilderで文字列を追加していく System.Diagnostics.Stopwatch sw3 = new System.Diagnostics.Stopwatch(); sw3.Start(); for (int i = 0; i < 1000000; i++) { //Capacityを指定する System.Text.StringBuilder sb = new System.Text.StringBuilder(s1.Length * appendCount); for (int ai = 0; ai < appendCount; ai++) { sb.Append(s1); } string r3 = sb.ToString(); } sw3.Stop(); //StringBuilderの方が速くなったか調べる if (sw1.ElapsedTicks > sw3.ElapsedTicks) { Console.WriteLine(appendCount); break; } }
この場合は、大体5~6回位でStringBuilder.Appendメソッドの方が速くなりました。
「C# StringBuilder Performance Test」や「StringBuilder vs. String / Fast String Operations with .NET 2.0」にも同様のベンチマークがありますが、そちらも似たような結果でした。
StringBuilderを使用する利点は速度だけではありません。Stringクラスを使用した場合は、次々と新しいインスタンスを作成しメモリを割り当てますがすぐに必要なくなるため、ガベージコレクションが頻繁に実行されます。その点StringBuilderクラスを使用すれば、ガベージコレクションが実行される頻度を大幅に減らすことができます。具体例については、「Performance considerations for strings in C#」などで紹介されています。
「C# StringBuilder Mistake」によると、StringBuilder.Appendメソッドで追加する文字列を連結する必要がある時は、連結演算子やString.Concatメソッドで文字列を結合してからAppendを呼び出すのは間違いで、すべての文字列をAppendで結合するのが正しく、パフォーマンスもその方が良いということです。
以下のコードでは、始めに悪い例、次に良い例を示しています。
Dim s1 As String = "0" Dim s2 As String = "1" Dim s3 As String = "2" Dim s4 As String = "3" Dim s5 As String = "4" Dim sb As New System.Text.StringBuilder() '悪い例 sb.Append(s1 & s2 & s3 & s4 & s5) '良い例 sb.Append(s1) sb.Append(s2) sb.Append(s3) sb.Append(s4) sb.Append(s5) 'または、次のようにも書ける sb.Append(s1).Append(s2).Append(s3).Append(s4).Append(s5)
string s1 = "0"; string s2 = "1"; string s3 = "2"; string s4 = "3"; string s5 = "4"; System.Text.StringBuilder sb = new System.Text.StringBuilder(); //悪い例 sb.Append(s1 + s2 + s3 + s4 + s5); //良い例 sb.Append(s1); sb.Append(s2); sb.Append(s3); sb.Append(s4); sb.Append(s5); //または、次のようにも書ける sb.Append(s1).Append(s2).Append(s3).Append(s4).Append(s5);
以下のようなコードで速度を比べてみると、確かに良いとされている方法の方が速いことが分かります。
Dim s1 As String = "0" Dim s2 As String = "1" Dim s3 As String = "2" Dim s4 As String = "3" Dim s5 As String = "4" 'StringBuilderのCapacityを指定せずに文字列を追加していく Dim sw1 As New System.Diagnostics.Stopwatch() sw1.Start() Dim sb1 As New System.Text.StringBuilder() For i As Integer = 0 To 10000000 sb1.Append(s1 & s2 & s3 & s4 & s5) Next Dim str1 As String = sb1.ToString() sw1.Stop() 'かかった時間を表示 Console.WriteLine("+してからAppend: {0}", sw1.ElapsedTicks) 'StringBuilderのCapacityを指定して文字列を追加していく Dim sw2 As New System.Diagnostics.Stopwatch() sw2.Start() Dim sb2 As New System.Text.StringBuilder() For i As Integer = 0 To 10000000 sb2.Append(s1) sb2.Append(s2) sb2.Append(s3) sb2.Append(s4) sb2.Append(s5) Next Dim str2 As String = sb2.ToString() sw2.Stop() 'かかった時間を表示 Console.WriteLine("すべてAppendで数行: {0}", sw2.ElapsedTicks) 'StringBuilderのCapacityを指定して文字列を追加していく Dim sw3 As New System.Diagnostics.Stopwatch() sw3.Start() Dim sb3 As New System.Text.StringBuilder() For i As Integer = 0 To 10000000 sb3.Append(s1).Append(s2).Append(s3).Append(s4).Append(s5) Next Dim str3 As String = sb3.ToString() sw3.Stop() 'かかった時間を表示 Console.WriteLine("すべてAppendで1行: {0}", sw3.ElapsedTicks) '結果例 '+してからAppend: 5387393 'すべてAppendで数行: 2968454 'すべてAppendで1行: 3012334
string s1 = "0"; string s2 = "1"; string s3 = "2"; string s4 = "3"; string s5 = "4"; //StringBuilderのCapacityを指定せずに文字列を追加していく System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch(); sw1.Start(); System.Text.StringBuilder sb1 = new System.Text.StringBuilder(); for (int i = 0; i < 10000000; i++) { sb1.Append(s1 + s2 + s3 + s4 + s5); } string str1 = sb1.ToString(); sw1.Stop(); //かかった時間を表示 Console.WriteLine("+してからAppend: {0}", sw1.ElapsedTicks); //StringBuilderのCapacityを指定して文字列を追加していく System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch(); sw2.Start(); System.Text.StringBuilder sb2 = new System.Text.StringBuilder(); for (int i = 0; i < 10000000; i++) { sb2.Append(s1); sb2.Append(s2); sb2.Append(s3); sb2.Append(s4); sb2.Append(s5); } string str2 = sb2.ToString(); sw2.Stop(); //かかった時間を表示 Console.WriteLine("すべてAppendで数行: {0}", sw2.ElapsedTicks); //StringBuilderのCapacityを指定して文字列を追加していく System.Diagnostics.Stopwatch sw3 = new System.Diagnostics.Stopwatch(); sw3.Start(); System.Text.StringBuilder sb3 = new System.Text.StringBuilder(); for (int i = 0; i < 10000000; i++) { sb3.Append(s1).Append(s2).Append(s3).Append(s4).Append(s5); } string str3 = sb3.ToString(); sw3.Stop(); //かかった時間を表示 Console.WriteLine("すべてAppendで1行: {0}", sw3.ElapsedTicks); //結果例 //+してからAppend: 5387393 //すべてAppendで数行: 2968454 //すべてAppendで1行: 3012334
ただし、良いとされる方法が常に速いとは言えないようです。「文字列を連結する」で説明したように、5つ以上の文字列を一度に連結する場合は、4つ以下の文字列を連結する場合よりもなかりパフォーマンスが落ちます。実際に私が試した限りでは、4つの文字を連結する場合は、悪いとされている方法のほうが若干速くなりました。
注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。