String.JoinとStringBuilder:どちらが速いですか?


80

前の質問Aのフォーマットについてdouble[][]CSV形式に、それが示唆された使用していることStringBuilderよりも速くなりますString.Join。これは本当ですか?


読者にわかりやすくするために、単一のStringBuilderを使用するのではなく、複数のstring.Joinを使用して結合しました(n + 1結合)
MarcGravell

2
パフォーマンスの違いはすぐに数桁に達します。一握り以上の結合を行う場合は、stringbuilderに切り替えることで多くのパフォーマンスを得ることができます
jalf

回答:


116

簡単な答え:状況によります。

長い答え:(区切り文字を使用して)連結する文字列の配列がすでにある場合String.Joinは、それを行う最も速い方法です。

String.Joinすべての文字列を調べて必要な正確な長さを計算してから、もう一度行ってすべてのデータをコピーできます。これは余分なコピーが必要ないことを意味します。唯一の欠点は、それが潜在的にメモリキャッシュに必要以上の時間を吹いているの手段、二回の文字列を通過しなければならないことです。

事前に文字列を配列として持っていない場合は、おそらく使用する方が速いでしょうStringBuilderが、そうでない場合もあります。StringBuilder大量のコピーを実行する手段を使用する場合は、配列を作成してから呼び出す方String.Joinがはるかに高速な場合があります。

編集:これは、への単一の呼び出しとへString.Joinの一連の呼び出しの観点からStringBuilder.Appendです。元の質問では、2つの異なるレベルのString.Join呼び出しがあったため、ネストされた呼び出しのそれぞれが中間文字列を作成していました。言い換えれば、それはさらに複雑で、推測するのが難しいのです。どちらの方法でも、一般的なデータで(複雑さの点で)大幅に「勝つ」のを見て驚かれることでしょう。

編集:私が家にいるとき、私は可能な限り苦痛なベンチマークを書きますStringBuilder。基本的に、各要素が前の要素の約2倍のサイズである配列があり、それが適切に取得された場合、すべての追加(区切り文字ではなく要素の)に対してコピーを強制できるはずですが、考慮にも入れてください)。その時点では、単純な文字列の連結とほぼ同じくらい悪いString.Joinですが、問題はありません。


6
事前に文字列がない場合でも、String.Joinを使用する方が速いようです。...私の答えを確認してください
Hosamアリー

2
配列の作成方法やサイズなどによって異なります。「<this>の場合、String.Joinは少なくとも同じくらい高速になります」というかなり明確な情報を提供できてうれしいです。逆行する。
Jon Skeet

4
(特に、StringBuilderがString.Joinを打ち負かすMarcの答えを見てください。人生は複雑です。)
Jon Skeet

2
@BornToCode:StringBuilder元の文字列を使用してを作成し、Append一度呼び出すという意味ですか?はい、私はstring.Joinそこで勝つことを期待しています。
Jon Skeet 2014年

13
[スレッドネクロマンシー]:現在の(.NET 4.5)string.Join使用の実装StringBuilder
n0rd 2016

31

これが私のテストリグint[][]です。簡単にするために使用しています。最初の結果:

Join: 9420ms (chk: 210710000
OneBuilder: 9021ms (chk: 210710000

double結果の更新:)

Join: 11635ms (chk: 210710000
OneBuilder: 11385ms (chk: 210710000

(2048 * 64 * 150を更新)

Join: 11620ms (chk: 206409600
OneBuilder: 11132ms (chk: 206409600

OptimizeForTestingが有効になっている場合:

Join: 11180ms (chk: 206409600
OneBuilder: 10784ms (chk: 206409600

それほど速くはありませんが、それほど大きくはありません。リグ(コンソールで実行、リリースモードなど):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Collect()
        {
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
        }
        static void Main(string[] args)
        {
            const int ROWS = 500, COLS = 20, LOOPS = 2000;
            int[][] data = new int[ROWS][];
            Random rand = new Random(123456);
            for (int row = 0; row < ROWS; row++)
            {
                int[] cells = new int[COLS];
                for (int col = 0; col < COLS; col++)
                {
                    cells[col] = rand.Next();
                }
                data[row] = cells;
            }
            Collect();
            int chksum = 0;
            Stopwatch watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOPS; i++)
            {
                chksum += Join(data).Length;
            }
            watch.Stop();
            Console.WriteLine("Join: {0}ms (chk: {1}", watch.ElapsedMilliseconds, chksum);

            Collect();
            chksum = 0;
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOPS; i++)
            {
                chksum += OneBuilder(data).Length;
            }
            watch.Stop();
            Console.WriteLine("OneBuilder: {0}ms (chk: {1}", watch.ElapsedMilliseconds, chksum);

            Console.WriteLine("done");
            Console.ReadLine();
        }
        public static string Join(int[][] array)
        {
            return String.Join(Environment.NewLine,
                    Array.ConvertAll(array,
                      row => String.Join(",",
                        Array.ConvertAll(row, x => x.ToString()))));
        }
        public static string OneBuilder(IEnumerable<int[]> source)
        {
            StringBuilder sb = new StringBuilder();
            bool firstRow = true;
            foreach (var row in source)
            {
                if (firstRow)
                {
                    firstRow = false;
                }
                else
                {
                    sb.AppendLine();
                }
                if (row.Length > 0)
                {
                    sb.Append(row[0]);
                    for (int i = 1; i < row.Length; i++)
                    {
                        sb.Append(',').Append(row[i]);
                    }
                }
            }
            return sb.ToString();
        }
    }
}

マークに感謝します。より大きなアレイでは何が得られますか?たとえば、[2048] [64]を使用しています(約1MB)。また、OptimizeForTesting()私が使用している方法を使用した場合、結果はどうにか異なりますか?
Hosam Aly

どうもありがとうマーク。しかし、マイクロベンチマークで異なる結果が得られるのはこれが初めてではないことに気づきました。なぜそうなるのか分かりますか?
Hosam Aly

2
カルマ?宇宙線?誰が知っているか...それはマイクロ最適化の危険性を示していますが;-p
MarcGravell

たとえば、AMDプロセッサを使用していますか?ET64?キャッシュメモリが少なすぎる(512 KB)のでしょうか?または、Windows Vistaの.NETフレームワークは、XP SP3のフレームワークよりも最適化されていますか?どう思いますか?私は本当に...なぜこれが起こっているに興味がある
Hosamアリーを

XP SP3、x86、Intel Core2 Duo T7250 @ 2GHz
MarcGravell

20

私はそうは思いません。Reflectorを見ると、の実装はString.Join非常に最適化されているように見えます。また、作成する文字列の合計サイズを事前に知っているという追加の利点もあるため、再割り当ては必要ありません。

それらを比較するために、2つのテストメソッドを作成しました。

public static string TestStringJoin(double[][] array)
{
    return String.Join(Environment.NewLine,
        Array.ConvertAll(array,
            row => String.Join(",",
                       Array.ConvertAll(row, x => x.ToString()))));
}

public static string TestStringBuilder(double[][] source)
{
    // based on Marc Gravell's code

    StringBuilder sb = new StringBuilder();
    foreach (var row in source)
    {
        if (row.Length > 0)
        {
            sb.Append(row[0]);
            for (int i = 1; i < row.Length; i++)
            {
                sb.Append(',').Append(row[i]);
            }
        }
    }
    return sb.ToString();
}

サイズの配列を渡して、各メソッドを50回実行しました[2048][64]。私はこれを2つのアレイに対して行いました。1つはゼロで埋められ、もう1つはランダムな値で埋められます。私のマシンでは次の結果が得られました(P4 3.0 GHz、シングルコア、HTなし、CMDからリリースモードを実行):

// with zeros:
TestStringJoin    took 00:00:02.2755280
TestStringBuilder took 00:00:02.3536041

// with random values:
TestStringJoin    took 00:00:05.6412147
TestStringBuilder took 00:00:05.8394650

配列のサイズをに増やし、[2048][512]反復回数を10に減らすと、次の結果が得られました。

// with zeros:
TestStringJoin    took 00:00:03.7146628
TestStringBuilder took 00:00:03.8886978

// with random values:
TestStringJoin    took 00:00:09.4991765
TestStringBuilder took 00:00:09.3033365

結果は再現可能です(ほとんど;異なるランダム値によって引き起こされる小さな変動があります)。どうやらString.Joinほとんどの場合少し速いです(非常にわずかなマージンですが)。

これは私がテストに使用したコードです:

const int Iterations = 50;
const int Rows = 2048;
const int Cols = 64; // 512

static void Main()
{
    OptimizeForTesting(); // set process priority to RealTime

    // test 1: zeros
    double[][] array = new double[Rows][];
    for (int i = 0; i < array.Length; ++i)
        array[i] = new double[Cols];

    CompareMethods(array);

    // test 2: random values
    Random random = new Random();
    double[] template = new double[Cols];
    for (int i = 0; i < template.Length; ++i)
        template[i] = random.NextDouble();

    for (int i = 0; i < array.Length; ++i)
        array[i] = template;

    CompareMethods(array);
}

static void CompareMethods(double[][] array)
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    for (int i = 0; i < Iterations; ++i)
        TestStringJoin(array);
    stopwatch.Stop();
    Console.WriteLine("TestStringJoin    took " + stopwatch.Elapsed);

    stopwatch.Reset(); stopwatch.Start();
    for (int i = 0; i < Iterations; ++i)
        TestStringBuilder(array);
    stopwatch.Stop();
    Console.WriteLine("TestStringBuilder took " + stopwatch.Elapsed);

}

static void OptimizeForTesting()
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process currentProcess = Process.GetCurrentProcess();
    currentProcess.PriorityClass = ProcessPriorityClass.RealTime;
    if (Environment.ProcessorCount > 1) {
        // use last core only
        currentProcess.ProcessorAffinity
            = new IntPtr(1 << (Environment.ProcessorCount - 1));
    }
}

13

プログラム全体の実行にかかる時間の点で1%の違いが重要なものにならない限り、これはマイクロ最適化のように見えます。最も読みやすく理解しやすいコードを記述し、1%のパフォーマンスの違いについて心配する必要はありません。


1
String.Joinの方が理解しやすいと思いますが、投稿はもっと楽しいチャレンジでした。:)直感的に別の方法が示唆されている場合でも、いくつかの組み込みメソッドを使用する方が、手動で行うよりも優れている可能性があることを学ぶことも役立ちます(IMHO)。...
Hosam Aly

...通常、多くの人がStringBuilderの使用を提案します。String.Joinの速度が1%遅いことがわかったとしても、StringBuilderの方が速いと思っているからといって、多くの人はそれについて考えていなかったでしょう。
Hosam Aly

調査に問題はありませんが、回答が得られたので、パフォーマンスが最優先事項であるかどうかはわかりません。ストリームに書き出す以外にCSVで文字列を作成する理由は考えられるので、おそらく中間文字列はまったく作成しません。
tvanfosson 2009


-3

はい。2つ以上の結合を行うと、はるかに高速になります。

string.joinを実行する場合、ランタイムは次のことを行う必要があります。

  1. 結果の文字列にメモリを割り当てます
  2. 最初の文字列の内容を出力文字列の先頭にコピーします
  3. 2番目の文字列の内容を出力文字列の最後にコピーします。

2つの結合を行う場合、データを2回コピーする必要があります。

StringBuilderは、スペースに余裕のある1つのバッファーを割り当てるため、元の文字列をコピーせずにデータを追加できます。バッファに空きが残っているので、追加した文字列を直接バッファに書き込むことができます。次に、最後に文字列全体を1回コピーするだけです。


1
ただし、String.Joinは事前に割り当て量を認識していますが、StringBuilderは認識していません。詳細については、私の回答を参照してください。
Aly

@erikkallen:リフレクターでString.Joinのコードを見ることができます。red-gate.com/products/reflector/index.htm
Aly
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.