Є String.Format настільки ж ефективним, як StringBuilder


160

Припустимо, у мене в C # є строкобудівник, який робить це:

StringBuilder sb = new StringBuilder();
string cat = "cat";
sb.Append("the ").Append(cat).(" in the hat");
string s = sb.ToString();

це було б настільки ж ефективно чи будь-яке більш ефективно, як:

string cat = "cat";
string s = String.Format("The {0} in the hat", cat);

Якщо так, то чому?

EDIT

Після кількох цікавих відповідей я зрозумів, що, мабуть, я повинен був бути трохи зрозумілішим у тому, що я просив. Я не так багато просив, що було швидше при об'єднанні рядка, але яке швидше введення однієї струни в іншу.

В обох випадках вище я хочу вставити одну або кілька рядків у середину попередньо визначеної рядки шаблону.

Вибачте за непорозуміння


Будь ласка, залиште їх відкритими, щоб дозволити майбутні удосконалення
Марк Бік

4
У спеціальному випадку найшвидший не є жодним із них: якщо частина, яку потрібно замінити, має розмір, що дорівнює новій частині, ви можете змінити рядок на місці. На жаль, для цього потрібне відображення або небезпечний код і свідомо порушує незмінність рядка. Недобра практика, але якщо швидкість - це проблема ... :)
Abel

у наведеному вище прикладі string s = "The "+cat+" in the hat";може бути найшвидшим, якщо він не використовується в циклі, в цьому випадку найшвидший буде з StringBuilder ініціалізацією поза циклом.
Сурія Пратап

Відповіді:


146

ПРИМІТКА. Ця відповідь була написана, коли .NET 2.0 була поточною версією. Це більше не може застосовуватися до пізніших версій.

String.Formatвикористовує StringBuilderвнутрішньо:

public static string Format(IFormatProvider provider, string format, params object[] args)
{
    if ((format == null) || (args == null))
    {
        throw new ArgumentNullException((format == null) ? "format" : "args");
    }

    StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
    builder.AppendFormat(provider, format, args);
    return builder.ToString();
}

Вищевказаний код є фрагментом від mscorlib, тому питання стає " StringBuilder.Append()швидше, ніж StringBuilder.AppendFormat()"?

Без бенчмаркінгу, напевно, я б сказав, що приклад коду, описаний вище, буде працювати швидше .Append(). Але це здогад, спробуйте тестування та / або профілювання двох, щоб отримати правильне порівняння.

Цей джеррі, Джеррі Діксон, зробив кілька еталонів:

http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm

Оновлено:

На жаль, посилання вище з тих пір померло. Однак на машині Way Back ще є копія:

http://web.archive.org/web/20090417100252/http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm

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


8
Одна з проблем із орієнтирами на сторінці Джеррі Діксона полягає в тому, що він ніколи не дзвонить .ToString()на StringBuilderоб’єкт. На протязі багатьох ітерацій цей час має велике значення, і це означає, що він не зовсім порівнює яблука з яблуками. Ось чому він показує таку чудову виставу StringBuilderі, ймовірно, пояснює свою несподіванку. Я просто повторив тест з виправленням цієї помилки і отримав очікувані результати: String +оператор був самим швидким, а потім StringBuilder, з String.Formatзамикаючим.
Бен Коллінз

5
Через 6 років це вже не зовсім так. У Net4, string.Format () створює та кешує екземпляр StringBuilder, який він повторно використовує, тому в деяких тестових випадках він може бути швидшим, ніж StringBuilder. Я поставив переглянутий орієнтир у відповідь нижче (який все ще говорить, що concat є найшвидшим, і для мого тестового випадку формат на 10% повільніше, ніж StringBuilder).
Кріс Ф Керролл

45

З документації MSDN :

Виконання операції об'єднання для об'єкта String або StringBuilder залежить від того, як часто відбувається розподіл пам'яті. Операція конкатенації рядків завжди виділяє пам'ять, тоді як операція конкатенації StringBuilder виділяє пам'ять лише в тому випадку, якщо буфер об'єктів StringBuilder занадто малий для розміщення нових даних. Отже, клас String є кращим для операції конкатенації, якщо фіксована кількість об'єктів String є об'єднаною. У цьому випадку окремі операції конкатенації можуть навіть поєднуватись у одну операцію компілятором. Об'єкт StringBuilder є кращим для операції конкатенації, якщо довільне число рядків об'єднано; наприклад, якщо цикл об'єднує випадкову кількість рядків введення користувача.


12

Я запустив кілька швидких показників продуктивності, і для 100 000 операцій в середньому за 10 запусків перший метод (String Builder) займає майже половину часу другого (String Format).

Отже, якщо це нечасто, це не має значення. Але якщо це звичайна операція, то, можливо, ви захочете скористатися першим методом.


10

Я б очікував, що String.Format буде повільнішим - він повинен проаналізувати рядок, а потім з'єднати його.

Пара приміток:

  • Формат - це спосіб знайти видимі для користувача рядки в професійних програмах; це дозволяє уникнути помилок локалізації
  • Якщо ви заздалегідь знаєте довжину результуючого рядка, скористайтеся конструктором StringBuilder (Int32), щоб заздалегідь визначити ємність

8

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

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

З іншого боку, якщо ви говорите про великий шматок статичного тексту з двома-трьома змінними в ньому, навіть якщо він трохи менш ефективний, я думаю, що чіткість, яку ви отримуєте від рядка. Формат робить це вартим того. Я використовував це раніше цього тижня, коли потрібно було розмістити один біт динамічного тексту в центрі документа на 4 сторінки. Оновити цей великий шматок тексту буде простіше, якщо його в одному фрагменті, ніж потрібно оновлювати три фрагменти, які ви об'єднаєте разом.


Так! Використовуйте String.Format, коли це має сенс, тобто коли ви форматуєте рядки. Використовуйте рядкове конкатенацію або StringBuilder, коли ви виконуєте механічну конкатенацію. Завжди прагніть вибрати метод, який повідомляє ваш намір наступному обслуговуючому персоналу.
Роб

8

Хоча б тому, що string.Format точно не робить те, що ви можете подумати, ось повтор тестів через 6 років на Net45.

Concat все ще найшвидший, але насправді це менше 30% різниці. StringBuilder і Format відрізняються ледь на 5-10%. Я кілька разів змінював тести на 20%.

Мільсекунд, мільйон ітерацій:

  • Сполучення: 367
  • Новий stringBuilder для кожного ключа: 452
  • Кешоване StringBuilder: 419
  • рядок.Формат: 475

Урок, який я забираю, полягає в тому, що різниця в продуктивності є тривіальною, і тому це не повинно зупиняти вас на написанні найпростішого читаного коду, який ви можете. Що за мої гроші часто, але не завжди a + b + c.

const int iterations=1000000;
var keyprefix= this.GetType().FullName;
var maxkeylength=keyprefix + 1 + 1+ Math.Log10(iterations);
Console.WriteLine("KeyPrefix \"{0}\", Max Key Length {1}",keyprefix, maxkeylength);

var concatkeys= new string[iterations];
var stringbuilderkeys= new string[iterations];
var cachedsbkeys= new string[iterations];
var formatkeys= new string[iterations];

var stopwatch= new System.Diagnostics.Stopwatch();
Console.WriteLine("Concatenation:");
stopwatch.Start();

for(int i=0; i<iterations; i++){
    var key1= keyprefix+":" + i.ToString();
    concatkeys[i]=key1;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("New stringBuilder for each key:");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2= new StringBuilder(keyprefix).Append(":").Append(i.ToString()).ToString();
    stringbuilderkeys[i]= key2;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("Cached StringBuilder:");
var cachedSB= new StringBuilder(maxkeylength);
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2b= cachedSB.Clear().Append(keyprefix).Append(":").Append(i.ToString()).ToString();
    cachedsbkeys[i]= key2b;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("string.Format");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key3= string.Format("{0}:{1}", keyprefix,i.ToString());
    formatkeys[i]= key3;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

var referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway= concatkeys.Union(stringbuilderkeys).Union(cachedsbkeys).Union(formatkeys).LastOrDefault(x=>x[1]=='-');
Console.WriteLine(referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway);

2
Під "string.Format точно не робить те, що можна подумати", я маю на увазі, що у вихідному коді 4.5 він намагається створити та повторно використовувати кешований екземпляр StringBuilder. Тож я включив такий підхід у тест
Кріс Ф Керролл

6

String.Format використовує StringBuilderвнутрішньо ... настільки логічно, що призводить до думки, що це було б трохи менш ефективно за рахунок більших витрат. Однак проста конкатенація рядків - це найшвидший метод введення однієї струни між двома іншими ... на значну міру. Ці докази продемонстрував Ріко Мар'яні у своїй першій вікторині на виставу років тому. Простий факт полягає в тому, що з'єднання ... коли кількість рядкових рядків відома (без обмежень ... ти можеш об'єднати тисячу частин ... до тих пір, як ти знаєш, її завжди 1000 частин) ... завжди швидше ніж StringBuilderабо String. Формат. Їх можна виконати за допомогою одного розподілу пам'яті на серії копій пам'яті. Ось доказ

А ось фактичний код для деяких методів String.Concat, який в кінцевому підсумку викликає FillStringChecked, який використовує покажчики для копіювання пам'яті (витягнутої через Reflector):

public static string Concat(params string[] values)
{
    int totalLength = 0;

    if (values == null)
    {
        throw new ArgumentNullException("values");
    }

    string[] strArray = new string[values.Length];

    for (int i = 0; i < values.Length; i++)
    {
        string str = values[i];
        strArray[i] = (str == null) ? Empty : str;
        totalLength += strArray[i].Length;

        if (totalLength < 0)
        {
            throw new OutOfMemoryException();
        }
    }

    return ConcatArray(strArray, totalLength);
}

public static string Concat(string str0, string str1, string str2, string str3)
{
    if (((str0 == null) && (str1 == null)) && ((str2 == null) && (str3 == null)))
    {
        return Empty;
    }

    if (str0 == null)
    {
        str0 = Empty;
    }

    if (str1 == null)
    {
        str1 = Empty;
    }

    if (str2 == null)
    {
        str2 = Empty;
    }

    if (str3 == null)
    {
        str3 = Empty;
    }

    int length = ((str0.Length + str1.Length) + str2.Length) + str3.Length;
    string dest = FastAllocateString(length);
    FillStringChecked(dest, 0, str0);
    FillStringChecked(dest, str0.Length, str1);
    FillStringChecked(dest, str0.Length + str1.Length, str2);
    FillStringChecked(dest, (str0.Length + str1.Length) + str2.Length, str3);
    return dest;
}

private static string ConcatArray(string[] values, int totalLength)
{
    string dest = FastAllocateString(totalLength);
    int destPos = 0;

    for (int i = 0; i < values.Length; i++)
    {
        FillStringChecked(dest, destPos, values[i]);
        destPos += values[i].Length;
    }

    return dest;
}

private static unsafe void FillStringChecked(string dest, int destPos, string src)
{
    int length = src.Length;

    if (length > (dest.Length - destPos))
    {
        throw new IndexOutOfRangeException();
    }

    fixed (char* chRef = &dest.m_firstChar)
    {
        fixed (char* chRef2 = &src.m_firstChar)
        {
            wstrcpy(chRef + destPos, chRef2, length);
        }
    }
}

Так то:

string what = "cat";
string inthehat = "The " + what + " in the hat!";

Насолоджуйтесь!


в Net4, string.Format кешує та повторно використовує екземпляр StringBuilder, тому в деяких випадках використання може бути швидшим.
Кріс Ф Керролл

3

Так само, найшвидшим буде:

string cat = "cat";
string s = "The " + cat + " in the hat";

ні, конкатенація рядків надзвичайно повільна, оскільки .NET створює додаткові копії ваших рядкових змінних між операціями concat, в цьому випадку: дві додаткові копії плюс остаточну копію для призначення. Результат: надзвичайно низька продуктивність порівняно з StringBuilderякою зроблена в першу чергу для оптимізації цього типу кодування.
Авель

Найшвидший
набір,

2
@Abel: У відповіді можуть бути відсутні деталі, але цей підхід є найшвидшим варіантом у цьому конкретному прикладі. Компілятор перетворить це в один виклик String.Concat (), тому заміна на StringBuilder фактично уповільнить код.
Дан К.

1
@Vaibhav вірно: у цьому випадку конкатенація найшвидша. Звичайно, різниця була б незначною, якщо б не повторювались багато разів, або, можливо, оперували над набагато більшими струнами.
Бен Коллінз

0

Це дійсно залежить. Для невеликих рядків з кількома конкатенатами насправді швидше просто додати рядки.

String s = "String A" + "String B";

Але для більшої струни (дуже дуже великих рядків) тоді ефективніше використовувати StringBuilder.


0

В обох випадках вище я хочу вставити одну або кілька рядків у середину попередньо визначеної рядки шаблону.

У такому випадку я б запропонував String.Format є найшвидшим, оскільки це дизайн саме для цієї мети.



-1

Я б не запропонував, оскільки String.Format не розрахований на конкатенацію, це був дизайн для форматування виводу різних входів, таких як дата.

String s = String.Format("Today is {0:dd-MMM-yyyy}.", DateTime.Today);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.