String.Join против StringBuilder: что быстрее?
В предыдущем вопросе о форматировании double[][]
в формат CSV предполагалось, что использование StringBuilder
будет быстрее, чем String.Join
. Это правда?
Ответов (5)5
Короткий ответ: это зависит от обстоятельств.
Long answer: if you already have an array of strings to concatenate together (with a delimiter), String.Join
is the fastest way of doing it.
String.Join
can look through all of the strings to work out the exact length it needs, then go again and copy all the data. This means there will be no extra copying involved. The only downside is that it has to go through the strings twice, which means potentially blowing the memory cache more times than necessary.
Если вы не имеете строки в виде массива заранее, это , вероятно , быстрее использовать StringBuilder
- но будут ситуации , когда это не так . Если вы используете StringBuilder
средства, выполняющие много-много копий, то создание массива и последующий вызов String.Join
могут быть быстрее.
РЕДАКТИРОВАТЬ: это с точки зрения одного вызова String.Join
против нескольких вызовов StringBuilder.Append
. В исходном вопросе у нас было два разных уровня String.Join
вызовов, поэтому каждый из вложенных вызовов создавал бы промежуточную строку. Другими словами, это еще сложнее, и о чем труднее догадаться. Я был бы удивлен, увидев, что любой из этих способов значительно (с точки зрения сложности) "выиграет" с типичными данными.
РЕДАКТИРОВАТЬ: Когда я буду дома, я напишу тест, который будет настолько болезненным, насколько это возможно StringBuilder
. В основном, если у вас есть массив, в котором каждый элемент примерно в два раза больше предыдущего, и вы все правильно понимаете, вы должны иметь возможность принудительно копировать для каждого добавления (элементов, а не разделителя, хотя это должно тоже следует учитывать). На этом этапе это почти так же плохо, как простая конкатенация строк, но не String.Join
будет проблем.
Я так не думаю. Просматривая Reflector, реализация String.Join
выглядит очень оптимизированной. Он также имеет дополнительное преимущество: заранее известно общий размер создаваемой строки, поэтому перераспределение не требуется.
Я создал два метода тестирования, чтобы сравнить их:
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]
. Я сделал это для двух массивов; один заполнен нулями, а другой заполнен случайными значениями. Я получил следующие результаты на своей машине (P4 3,0 ГГц, одноядерный, без HT, работает режим Release из 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
Увеличение размера массива до 10 [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, среда выполнения должна:
- Выделите память для результирующей строки
- скопируйте содержимое первой строки в начало выходной строки
- скопируйте содержимое второй строки в конец выходной строки.
Если вы выполняете два соединения, он должен скопировать данные дважды и так далее.
StringBuilder выделяет один буфер с свободным пространством, поэтому данные можно добавлять без необходимости копировать исходную строку. Поскольку в буфере осталось свободное место, добавленную строку можно записать в буфер напрямую. Затем ему просто нужно скопировать всю строку один раз, в конце.
Вот мой испытательный стенд, использующий 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
Так быстрее, но не так массово; rig (запускать на консоли, в режиме выпуска и т. д.):
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();
}
}
}