を CSV 形式にフォーマットすることに関する以前の質問では、 を使用するとを使用するよりも高速であることが示唆されました。これは本当ですか?double[][]
StringBuilder
String.Join
5 に答える
簡単な答え: 場合によります。
長い答え: (区切り記号を使用して) 連結する文字列の配列が既にある場合String.Join
は、それを行う最速の方法です。
String.Join
すべての文字列を調べて必要な正確な長さを計算し、もう一度行ってすべてのデータをコピーできます。これは、余分なコピーが必要ないことを意味します。唯一の欠点は、文字列を 2 回処理する必要があることです。これは、必要以上にメモリ キャッシュを吹き飛ばす可能性があることを意味します。
事前に文字列を配列として持っていない場合は、おそらく使用する方が高速ですが、そうでないStringBuilder
場合もあります。StringBuilder
大量のコピーを実行する手段を使用する場合は、配列を作成してから呼び出すString.Join
方が高速な場合があります。
編集: これは、 への 1 回の呼び出しと へString.Join
の一連の呼び出しの観点からStringBuilder.Append
です。元の質問では、2 つの異なるレベルのString.Join
呼び出しがあったため、ネストされた呼び出しのそれぞれが中間文字列を作成したことになります。言い換えれば、それはさらに複雑で、推測するのが難しい. 典型的なデータで、どちらの方法も(複雑さの点で)大幅に「勝つ」のを見ると驚かれることでしょう。
編集: 家にいるときは、StringBuilder
. 基本的に、各要素が前の要素の約 2 倍のサイズの配列を持っていて、それを適切に取得した場合、(区切り文字ではなく、要素の) 追加ごとにコピーを強制できるはずですが、それは必要です。も考慮されます)。その時点では、単純な文字列連結とほぼ同じくらい悪いString.Join
ですが、問題はありません。
int[][]
簡単にするために使用するテスト リグを次に示します。最初に結果:
Join: 9420ms (chk: 210710000
OneBuilder: 9021ms (chk: 210710000
(結果の更新double
:)
Join: 11635ms (chk: 210710000
OneBuilder: 11385ms (chk: 210710000
(更新 re 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();
}
}
}
私はそうは思わない。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();
}
size の配列を渡して、各メソッドを 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));
}
}
はい。複数の結合を行うと、はるかに高速になります。
string.join を実行する場合、ランタイムは次のことを行う必要があります。
- 結果の文字列にメモリを割り当てます
- 最初の文字列の内容を出力文字列の先頭にコピーします
- 2 番目の文字列の内容を出力文字列の末尾にコピーします。
2 つの結合を行う場合は、データを 2 回コピーする必要があります。
StringBuilder は 1 つのバッファーに余裕のあるスペースを割り当てるため、元の文字列をコピーしなくてもデータを追加できます。バッファーにはスペースが残っているため、追加された文字列をバッファーに直接書き込むことができます。次に、最後に文字列全体を 1 回コピーするだけです。