String.Join проти StringBuilder: що швидше?


80

У попередньому питанні про форматування double[][]формату CSV було висловлено припущення, що використання StringBuilderбуде швидшим, ніж String.Join. Це правда?


Для наочності читачів мова йшла про використання одного StringBuilder проти кількох рядків. Приєднуйтесь, які потім приєднувались (n + 1 приєднання)
Marc Gravell

2
Різниця у швидкодії швидко сягає кількох порядків. Якщо ви зробите більше, ніж кілька приєднань, ви можете отримати багато результатів, переключившись на будівельника струн
jalf

Відповіді:


116

Коротка відповідь: це залежить.

Довга відповідь: якщо у вас вже є масив рядків для об’єднання (з роздільником), String.Joinце найшвидший спосіб зробити це.

String.Joinможе переглянути всі рядки, щоб визначити точну довжину, яка йому потрібна, а потім знову скопіювати всі дані. Це означає , що не НЕ додаткове копіювання бере участь. Тільки недоліком є те, що він повинен пройти через рядків в два рази, що означає потенційно видування кеш - пам'яті більше часу , ніж це необхідно.

Якщо у вас немає рядків як масиву заздалегідь, можливо , це швидше використовувати, StringBuilderале траплятимуться ситуації, коли цього немає. Якщо за допомогою StringBuilderзасобу виконується багато-багато копій, то побудова масиву, а потім виклик String.Joinможе бути швидшим.

EDIT: Це стосується одного дзвінка до String.Joinгрупи дзвінків до StringBuilder.Append. У вихідному питанні у нас було два різних рівні String.Joinвикликів, тому кожен із вкладених викликів створив би проміжний рядок. Іншими словами, це ще складніше і про нього важче здогадатися. Я був би здивований, побачивши, що в будь-якому випадку суттєво "виграю" (з точки зору складності) із типовими даними.

EDIT: Коли я буду вдома, я напишу орієнтир, який є настільки болючим, наскільки можливо StringBuilder. В основному, якщо у вас є масив, де кожен елемент приблизно вдвічі перевищує розмір попереднього, і ви правильно його отримуєте, ви повинні мати змогу примусити копіювати кожен додаток (елементів, а не роздільника, хоча це потрібно також враховувати). На той момент це майже так само погано, як просте об'єднання рядків - але String.Joinпроблем не буде.


6
Навіть коли у мене немає рядків заздалегідь, здається, швидше використовувати String.Join. Перевірте мою відповідь ...
Хосам Алі

2
Залежно від того, як створюється масив, його розміру і т. Д. Я радий дати досить чітке "У <this випадку String.Join буде принаймні таким же швидким" - я не хотів би робити зворотний.
Джон Скіт,

4
(Зокрема, подивіться на відповідь Марка, де StringBuilder вибиває String.Join, майже. Життя складно.)
Джон Скіт,

2
@BornToCode: Ви маєте на увазі побудову a StringBuilderз оригінальним рядком, а потім Appendодин раз зателефонувати ? Так, я очікував string.Joinби там перемогти.
Джон Скіт,

13
[Некромантія нитки]: Поточна (.NET 4.5) реалізація string.Joinвикористання StringBuilder.
n0rd

31

Ось моя тестова установка, що використовується 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

Так швидше, але не масово; 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();
        }
    }
}

Дякую Марк. Що ви отримуєте за більші масиви? Наприклад, я використовую [2048] [64] (близько 1 МБ). Також чи відрізняються ваші результати, якщо ви використовуєте OptimizeForTesting()метод, який я використовую?
Хосам Алі

Велике спасибі, Марк. Але я помічаю, що це не перший раз, коли ми отримуємо різні результати щодо мікро-тестів. Ви уявляєте, чому це може бути?
Хосам Алі

2
Карма? Космічні промені? Хто знає ... все ж це показує небезпеку мікрооптимізації
;-p

Ви, наприклад, використовуєте процесор AMD? ET64? Можливо, у мене замало кеш-пам'яті (512 КБ)? А може .NET-платформа в Windows Vista є оптимізованою, ніж для XP SP3? Як ти гадаєш? Мене дуже цікавить, чому це відбувається ...
Хосам Алі

XP SP3, x86, Intel Core2 Duo T7250 @ 2GHz
Marc Gravell

20

Я не думаю. Переглядаючи 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, працює режим випуску з 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) дізнатися, що використання декількох вбудованих методів може бути кращим, ніж робити це вручну, навіть коли інтуїція може припустити інше. ...
Хосам Алі

... Зазвичай багато людей пропонують використовувати StringBuilder. Навіть якби String.Join виявився на 1% повільнішим, багато людей не думали б про це, лише тому, що вважають, що StringBuilder швидший.
Хосам Алі

Я не маю жодних проблем із розслідуванням, але тепер, коли ви отримали відповідь, я не впевнений, що продуктивність є головним питанням. Оскільки я можу придумати будь-яку причину для побудови рядка в CSV, окрім як записати його в потік, я, мабуть, взагалі не будував би проміжний рядок.
tvanfosson


-3

так. Якщо ви зробите більше, ніж пару об’єднань, це буде набагато швидше.

Коли ви робите string.join, час виконання повинен:

  1. Виділити пам’ять для отриманого рядка
  2. скопіюйте вміст першого рядка на початок вихідного рядка
  3. скопіюйте вміст другого рядка в кінець вихідного рядка.

Якщо ви зробите два об'єднання, він повинен скопіювати дані двічі тощо.

StringBuilder виділяє один буфер із вільним місцем, тому дані можна додавати без копіювання вихідного рядка. Оскільки в буфері залишається місце, доданий рядок може бути записаний безпосередньо в буфер. Тоді йому просто потрібно скопіювати весь рядок один раз, наприкінці.


1
Але String.Join заздалегідь знає, скільки виділити, тоді як StringBuilder - ні. Будь ласка, перегляньте мою відповідь для отримання додаткових роз’яснень.
Хосам Алі

@erikkallen: Ви можете побачити код для String.Join у Reflector. red-gate.com/products/reflector/index.htm
Хосам Алі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.