閉じる

【C#】string vs StringBuilder比較!速度と使い分け

C#における文字列操作は、日常的なプログラミングで最も頻繁に行われる処理の一つです。

しかし、何気なく使用しているstring型と、大量の文字列を扱う際に推奨されるStringBuilderクラスには、内部構造に大きな違いがあります。

この違いを理解せずにコードを書くと、アプリケーションのパフォーマンスが著しく低下</cst-redしたり、メモリ消費量が増大したりするリスクがあります。

本記事では、これら二つの特性を徹底的に比較し、モダンな.NET開発においてどちらを選択すべきか、具体的なベンチマークを交えて解説します。

文字列操作の基本:string型と不変性

C#のstring型は「不変(Immutable)」という特性を持っています。

これは、一度作成された文字列オブジェクトの内容を後から変更することができないことを意味します。

一見すると、変数の値を書き換えているように見えても、その裏側では新しいメモリ領域が確保され、全く別のオブジェクトが生成されています。

string型の連結処理で何が起きているのか

例えば、s += "追加テキスト"というコードを実行すると、システム内部では「現在の文字列」と「追加する文字列」を合わせた新しい文字列を格納するための新しいメモリ領域が割り当てられます。

元の文字列は不要になり、最終的にガベージコレクション(GC)によって回収されるのを待つ状態になります。

繰り返し連結によるパフォーマンス劣化

この挙動は、ループ処理の中で文字列を連結する場合に深刻な問題を引き起こします。

1,000回のループで文字列を連結すると、1,000個の新しい文字列オブジェクトが生成され、999個の不要なオブジェクトがメモリ上に残ります。

これはCPUリソースを浪費するだけでなく、メモリのフラグメンテーション(断片化)を引き起こす原因にもなります。

StringBuilderクラスの仕組みと可変性

対照的に、System.Text.StringBuilderは「可変(Mutable)」な文字列操作を提供します。

これは、内部に文字配列のバッファを持っており、そのバッファの中で直接文字を書き換える仕組みです。

バッファ管理による効率化

StringBuilderは、初期化時、あるいは必要に応じて一定の大きさのメモリをあらかじめ確保します。

文字列を追加(Append)する際、バッファに空きがあれば、新しいオブジェクトを作らずにその場でデータを追加します。

バッファが足りなくなった時だけ、より大きなバッファを確保し直すという戦略をとっています。

メモリ確保の戦略

デフォルトでは、バッファが不足すると現在の2倍のサイズに拡張されます。

この仕組みにより、頻繁なメモリ割り当てを避け、計算量を大幅に削減することが可能になっています。

大量の文字列連結において、StringBuilderが圧倒的に速い理由はここにあります。

速度比較ベンチマーク:string vs StringBuilder

理論上の違いを理解したところで、実際にどれほどのパフォーマンス差が出るのか、具体的なコードで検証してみましょう。

以下のサンプルプログラムでは、10,000回の文字列連結にかかる時間を計測します。

C#
using System;
using System.Diagnostics;
using System.Text;

class Program
{
    static void Main()
    {
        const int iterations = 10000;
        
        // stringによる連結の計測
        Stopwatch sw1 = Stopwatch.StartNew();
        string text = "";
        for (int i = 0; i < iterations; i++)
        {
            // 毎回新しいオブジェクトが生成される
            text += "a";
        }
        sw1.Stop();
        Console.WriteLine($"string (+=): {sw1.ElapsedMilliseconds} ms");

        // StringBuilderによる連結の計測
        Stopwatch sw2 = Stopwatch.StartNew();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < iterations; i++)
        {
            // 内部バッファに書き込む
            sb.Append("a");
        }
        string result = sb.ToString();
        sw2.Stop();
        Console.WriteLine($"StringBuilder: {sw2.ElapsedMilliseconds} ms");
    }
}
実行結果
string (+=): 45 ms
StringBuilder: 0 ms

※実行環境によって数値は変動しますが、オーダーの違いは明確です。

結果の考察

10,000回程度の連結であっても、stringによる連結は目に見えて時間がかかります。

これが100,000回になると、stringの場合は数秒から数十秒かかることもありますが、StringBuilderわずか数ミリ秒で完了します。

どちらを使うべきか?判断基準と使い分け

パフォーマンスの面ではStringBuilderが圧倒的に有利ですが、常にこれを使うべきというわけではありません。

コードの可読性やメンテナンス性、そして微小なオーバーヘッドを考慮する必要があります。

string型を選択すべきケース

以下のような単純なケースでは、string型の方が適しています。

  • 連結する回数が数回程度と分かっている場合。
  • $"Name: {name}, Age: {age}"のような文字列補間を使用する場合。
  • コードの短さと分かりやすさを優先したい場合。

モダンなC#コンパイラは、少数の連結(例えば3〜4個の変数の連結)であれば、内部的にstring.Concatに最適化するため、StringBuilderを明示的に使うメリットはほとんどありません。

StringBuilderを選択すべきケース

以下のような状況では、迷わずStringBuilderを使用してください。

  • ループ内で繰り返し文字列を連結する場合。
  • 連結する回数が実行時まで不明な場合。
  • 非常に長い巨大なテキストを構築する場合。

比較まとめ表

特徴string (+=)StringBuilder
性質不変(Immutable)可変(Mutable)
メモリ割り当て連結のたびに新しい領域を確保必要時のみバッファを拡張
速度(少量)高速(最適化されるため)わずかなオーバーヘッドあり
速度(大量)極めて低速極めて高速
主な用途定型文、少数の連結ループ処理、動的な文章構築

さらなる最適化:StringBuilderのキャパシティ指定

StringBuilderをさらに効率的に使うテクニックとして、初期キャパシティ(容量)の指定があります。

デフォルトの状態では、バッファが足りなくなるたびに「新しい大きなメモリ確保」と「古いデータのコピー」が発生します。

もし、最終的な文字列の長さがおおよそ予測できるのであれば、コンストラクタでそのサイズを指定することで、内部的なリサイズ処理をゼロに抑えることができます。

C#
// およそ1万文字になると予想される場合
StringBuilder sb = new StringBuilder(10000); 

for (int i = 0; i < 1000; i++)
{
    sb.Append("data line\n");
}

このように記述することで、メモリの断片化を最小限に抑え、ガベージコレクションの負荷をさらに軽減することが可能です。

モダンC#におけるその他の選択肢

最新の.NET環境(.NET 6/7/8以降)では、StringBuilder以外にも高性能な文字列操作の手法が登場しています。

ReadOnlySpan<char> とスタック割り当て

メモリ効率を極限まで高める必要がある場合、Span<T>ReadOnlySpan<char>が活用されます。

これらはスタックメモリを利用することで、ヒープへの割り当て(GCの対象となるメモリ確保)を完全に回避できる場合があります。

string.Create による高効率生成

特定の長さの文字列を一度だけ生成し、その中身を高速に埋めたい場合には、string.Createメソッドが有効です。

これは、不変であるはずのstringの内部バッファを生成直後の一瞬だけ直接操作</cst-redさせてくれる特殊なメソッドです。

パフォーマンス設計の勘所

ここまでStringBuilderの優位性を説明してきましたが、最も重要なのは「計測」することです。

小規模なアプリケーションであれば、stringの連結がボトルネックになることは稀です。

しかし、サーバーサイドの処理や、大量のログ出力、複雑なファイル生成などでは、この選択一つでシステム全体の応答性が変わります。

推奨される開発フロー

  1. まずは読みやすさを優先してstringや文字列補間を使用する。
  2. ループ処理が含まれる場合は、最初からStringBuilderを検討する。
  3. パフォーマンスが要求される箇所では、ベンチマークをとって最適な手法を選択する。

C#には、文字列連結以外にも多くの便利なメソッド(例えばstring.Joinなど)が用意されています。

これらも内部的には効率的なアルゴリズムが採用されているため、用途に応じて適切に使い分けていくことがプロフェッショナルなコーディングへの近道です。

まとめ

C#における文字列操作は、その「不変性」を知っているかどうかでコードの質が大きく変わります。

string型は使い勝手が良く、少数の連結には最適ですが、ループ内での繰り返し連結には不向きであるという点を常に意識しましょう。

一方でStringBuilderは、大量のデータ処理において劇的なパフォーマンス向上をもたらします。

内部バッファを効率的に活用し、不要なメモリ割り当てを抑えるその仕組みは、大規模なアプリケーション開発において欠かせない知識です。

最新の.NETでは、さらに高度なSpan<T>string.Createといった選択肢も増えています。

基本となるstringStringBuilderの使い分けをマスターした上で、必要に応じてこれらの高度な技術を取り入れ、高速でメモリ効率の良いアプリケーションを目指してください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!