DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

文字列処理を高速に行う

.NET Frameworkには文字列を表現するクラスとしてStringクラスがあります。Stringクラスを使うと、文字列の連結や置換、挿入などの処理を簡単に行うことができます。

補足:文字列の連結については「文字列を連結する」で、文字列の置換については「文字列を置換する」で、文字列の挿入については「文字列を挿入する」で説明しています。

Stringオブジェクトには、その内容を変更することができないという特徴があります。Stringオブジェクトを連結、置換、挿入する場合も実は、その度に新しいStringオブジェクトが作成されています。

これに対して内容を変更できる文字列を表現したクラスが、StringBuilderクラス(System.Text名前空間)です。StringBuilderオブジェクトに対して文字列の追加、置換、挿入を行うと、そのオブジェクトの内容が変更されるだけで、新しいオブジェクトを作成しません。そのため、同じ文字列に対してこれらの処理を複数回行う場合は、Stringクラスの代わりにStringBuilderクラスを使うことでパフォーマンスが向上するケースが多いです。特にループ内ではStringBuilderを使用するのが一般的です。

なおパフォーマンスについてより詳しくは、後述します。

次の例では、String、StringBuilderクラスのそれぞれを使って文字列を何回も追加し、処理にかかった時間を計測しています。

VB.NET
コードを隠すコードを選択
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ミリ秒
C#
コードを隠すコードを選択
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のパフォーマンスについてより詳しく説明します。なお以下の説明では特に文字列を追加するケースについて取り上げていますが、文字列の置換や挿入などでもほぼ同様だと思ってください。

StringBuilderのCapacityを指定しておく

StringBuilder.Appendで文字列を追加していく時、最終的にどの程度の長さになるか分かっているならば、その長さをStringBuilder.Capacityプロパティにあらかじめ指定しておくとパフォーマンスが向上します。

Capacityプロパティは、StringBuilderのバッファの容量を表します。StringBuilderに追加された文字列がこの容量を超えた時は、自動的により大きなバッファを割り当て、そこに今までのデータをコピーします。よって、Capacityの値が頻繁に更新されると効率が落ちるため、始めから必要なだけ容量を確保しておきます。

補足:Capacityの値は、必要に応じて倍々に増えていきます。

Capacityプロパティは、StringBuilderのコンストラクタにパラメータで指定することができます。Capacityを指定していなかった時は、16というとても小さい値になってしまいます。

Capacityを指定した時としなかった時でどの程度の差が出るかを、以下のようなコードで試してみました。ビックリするほどの差は出ませんでしたが、確かにCapacityを指定した方が速かったです。

VB.NET
コードを隠すコードを選択
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
C#
コードを隠すコードを選択
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

StringBuilderはループ内で使用する

文字列を連結する」では、複数の文字列を連結する時、連結演算子(または、String.Concatメソッド)で一度に連結した方が、StringBuilder.Appendメソッドで連結するよりも高速であることを紹介しました。しかし、例えばループなどによって文字列を1つずつ連結していく場合は、連結する文字列が増えるほど連結演算子よりStringBuilder.Appendメソッドの方が速くなります。よって、StringBuilder.Appendメソッドはループ内で使用するというのが基本です。

具体的に、一体何回連結を繰り返せば連結演算子よりもStringBuilder.Appendメソッドの方が速くなるかを以下のようなコードで試してみました。

VB.NET
コードを隠すコードを選択
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
C#
コードを隠すコードを選択
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を指定した時と比べてみました。

VB.NET
コードを隠すコードを選択
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
C#
コードを隠すコードを選択
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#」などで紹介されています。

追加する文字列を + してからAppendしない

C# StringBuilder Mistake」によると、StringBuilder.Appendメソッドで追加する文字列を連結する必要がある時は、連結演算子やString.Concatメソッドで文字列を結合してからAppendを呼び出すのは間違いで、すべての文字列をAppendで結合するのが正しく、パフォーマンスもその方が良いということです。

以下のコードでは、始めに悪い例、次に良い例を示しています。

VB.NET
コードを隠すコードを選択
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)
C#
コードを隠すコードを選択
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);

以下のようなコードで速度を比べてみると、確かに良いとされている方法の方が速いことが分かります。

VB.NET
コードを隠すコードを選択
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
C#
コードを隠すコードを選択
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つの文字を連結する場合は、悪いとされている方法のほうが若干速くなりました。

  • 履歴:
  • 2011/4/12 「StringBuilderのパフォーマンス」を追加。
  • 2011/5/15 説明の一部をより詳しく書き換えた。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • .NET Tipsをご利用いただく際は、注意事項をお守りください。