Строка.Присоединиться противСтроковый конструктор:что быстрее?

StackOverflow https://stackoverflow.com/questions/585860

Вопрос

В предыдущий вопрос о форматировании 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%.

У Этвуда был пост, отчасти связанный с этим, около месяца назад:

http://www.codinghorror.com/blog/archives/001218.html

ДА.Если вы выполните больше пары соединений, это будет очень много быстрее.

Когда вы выполняете string.join, среда выполнения должна:

  1. Выделите память для результирующей строки
  2. скопируйте содержимое первой строки в начало выходной строки
  3. скопируйте содержимое второй строки в конец выходной строки.

Если вы выполняете два соединения, ему приходится копировать данные дважды, и так далее.

StringBuilder выделяет один буфер с запасным пространством, поэтому данные могут быть добавлены без необходимости копировать исходную строку.Поскольку в буфере осталось свободное место, добавленная строка может быть записана непосредственно в буфер.Затем ему просто нужно скопировать всю строку один раз, в конце.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top