Строка.Присоединиться противСтроковый конструктор:что быстрее?
-
06-09-2019 - |
Вопрос
В предыдущий вопрос о форматировании double[][]
в формат CSV, это было предложено это использование StringBuilder
было бы быстрее, чем String.Join
.Это правда?
Решение
Короткий ответ:это зависит от обстоятельств.
Длинный ответ: если у вас уже есть массив строк для объединения вместе (с разделителем), String.Join
это самый быстрый способ сделать это.
String.Join
можете просмотреть все строки, чтобы определить точную необходимую длину, затем перейдите еще раз и скопируйте все данные.Это означает, что будет НЕТ требуется дополнительное копирование.В Только недостатком является то, что ему приходится проходить по строкам дважды, что означает потенциальное сжатие кэша памяти больше раз, чем необходимо.
Если вы не надо заранее иметь строки в виде массива, это вероятно более быстрый в использовании StringBuilder
- но будут ситуации, когда это не так.При использовании StringBuilder
означает делать много-много копий, затем создавать массив и затем вызывать String.Join
вполне может быть быстрее.
Редактировать:Это с точки зрения одного вызова к String.Join
против кучи звонков в StringBuilder.Append
.В исходном вопросе у нас было два разных уровня String.Join
вызовы, поэтому каждый из вложенных вызовов создал бы промежуточную строку.Другими словами, это еще сложнее, и об этом еще труднее догадаться.Я был бы удивлен, увидев, что любой из способов значительно "выигрывает" (с точки зрения сложности) при использовании типичных данных.
Редактировать:Когда я буду дома, я напишу контрольный показатель, который будет настолько болезненным, насколько это возможно для StringBuilder
.В принципе, если у вас есть массив, в котором каждый элемент примерно в два раза больше предыдущего, и вы получаете его в самый раз, вы должны иметь возможность принудительно копировать для каждого добавления (элементов, а не разделителя, хотя это тоже необходимо учитывать).На данный момент это почти так же плохо, как простая конкатенация строк, но String.Join
у вас не будет никаких проблем.
Другие советы
Вот моя испытательная установка, использующая 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
и с включенным оптимизационным тестированием:
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();
}
}
}
Я так не думаю.Глядя через Отражатель, реализация 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 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));
}
}
Если разница в 1% не превращается во что-то существенное с точки зрения времени, необходимого для запуска всей программы, это выглядит как микрооптимизация.Я бы написал код, который является наиболее читаемым / понятным, и не беспокоился бы о разнице в производительности в 1%.
У Этвуда был пост, отчасти связанный с этим, около месяца назад:
ДА.Если вы выполните больше пары соединений, это будет очень много быстрее.
Когда вы выполняете string.join, среда выполнения должна:
- Выделите память для результирующей строки
- скопируйте содержимое первой строки в начало выходной строки
- скопируйте содержимое второй строки в конец выходной строки.
Если вы выполняете два соединения, ему приходится копировать данные дважды, и так далее.
StringBuilder выделяет один буфер с запасным пространством, поэтому данные могут быть добавлены без необходимости копировать исходную строку.Поскольку в буфере осталось свободное место, добавленная строка может быть записана непосредственно в буфер.Затем ему просто нужно скопировать всю строку один раз, в конце.