StringBuilder.Append Vs StringBuilder.AppendFormat
Мне было интересно о StringBuilder, и у меня возник вопрос, что я надеюсь, что сообщество сможет объяснить.
Давайте просто забудем о читаемости кода, какая из них быстрее и почему?
StringBuilder.Append
:
StringBuilder sb = new StringBuilder();
sb.Append(string1);
sb.Append("----");
sb.Append(string2);
StringBuilder.AppendFormat
:
StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0}----{1}",string1,string2);
Ответы
Ответ 1
Невозможно сказать, не зная размера string1
и string2
.
При вызове AppendFormat
он будет предварительно распределять буфер только один раз с учетом длины строки формата и строк, которые будут быть вставлен и затем конкатенировать все и вставить его в буфер. Для очень больших строк это будет выгодно по отдельным вызовам Append
, что может привести к тому, что буфер будет расширяться несколько раз.
Однако три вызова Append
могут или не могут вызвать рост буфера, и эта проверка выполняется при каждом вызове. Если строки достаточно малы и не запускается расширение буфера, то это будет быстрее, чем вызов AppendFormat
, потому что ему не придется разбирать строку формата, чтобы выяснить, где делать замены.
Дополнительные данные необходимы для окончательного ответа
Следует отметить, что обсуждается использование статического метода Concat
в классе String
(Ответить Jon, используя AppendWithCapacity
, напомнил мне об этом). Его результаты теста показывают, что это лучший случай (если вам не нужно использовать специальный спецификатор формата). String.Concat
делает то же самое в том, что он будет предопределять длину строк для конкатенации и предварительного распределения буфера (с немного большей накладкой из-за циклических конструкций через параметры). Его производительность будет сопоставима с методом Jon AppendWithCapacity
.
Или просто оператор простой сложения, поскольку он компилируется для вызова String.Concat
в любом случае с предупреждением о том, что все дополнения находятся в одном выражении:
// One call to String.Concat.
string result = a + b + c;
НЕ
// Two calls to String.Concat.
string result = a + b;
result = result + c;
Для всех тех, кто устанавливает тестовый код
Вам нужно запускать тестовые примеры в отдельных прогонах (или, по крайней мере, выполнять GC между измерением отдельных тестовых прогонов). Причина в том, что если вы скажете, что 1,000,000 работает, создавая новый StringBuilder
на каждой итерации цикла для одного теста, и затем вы запускаете следующий тест, который петли столько же раз, создавая дополнительные 1,000,000 StringBuilder
экземпляров, GC, скорее всего, вступит во второй тест и затруднит его синхронизацию.
Ответ 2
casperOne верен. Как только вы достигнете определенного порога, метод Append()
будет медленнее, чем AppendFormat()
. Ниже приведены различные длины и истекшие тики 100 000 итераций каждого метода:
Длина: 1
Append() - 50900
AppendFormat() - 126826
Длина: 1000
Append() - 1241938
AppendFormat() - 1337396
Длина: 10 000
Append() - 12482051
AppendFormat() - 12740862
Длина: 20 000
Append() - 61029875
AppendFormat() - 60483914
Когда вводятся строки с длиной около 20 000, функция AppendFormat()
будет немного превосходить Append()
.
Почему это происходит? См. ответ casperOne.
Edit:
Я повторяю каждый тест отдельно в разделе "Конфигурация выпуска" и обновляю результаты.
Ответ 3
casperOne полностью точна, что это зависит от данных. Однако предположим, что вы пишете это как библиотеку классов для третьих сторон, чтобы потреблять - что бы вы использовали?
Одним из вариантов было бы получить лучшее из обоих миров - выяснить, сколько данных вы собираетесь добавить, а затем использовать StringBuilder.EnsureCapacity, чтобы убедиться, что нам нужен только один размер буфера.
Если бы я не был слишком обеспокоен, я бы использовал Append
x3 - кажется, что "скорее" будет быстрее, поскольку синтаксический анализ токенов строкового формата на каждом вызове явно выполнен.
Обратите внимание, что я попросил команду BCL создать своего рода "кешированный форматировщик", который мы могли бы создать с использованием строки формата, а затем повторно использовать повторно. Это безумие, что структура должна анализировать строку формата каждый раз, когда она используется.
EDIT: Хорошо, я немного изменил код Джона для удобства и добавил "AppendWithCapacity", который сначала сначала создает необходимую емкость. Вот результаты для разных длин - для длины 1 я использовал 1,000,000 итераций; для всех других длин я использовал 100 000. (Это было просто для того, чтобы получить разумное время работы.) Все времена в миллисе.
К сожалению, таблицы не работают в SO. Длины были 1, 1000, 10000, 20000
Время:
- Добавить: 162, 475, 7997, 17970
- AppendFormat: 392, 499, 8541, 18993
- AppendWithCapacity: 139, 189, 1558, 3085
Так как это случилось, я никогда не видел AppendFormat бить Append - но я действительно видел AppendWithCapacity с очень существенным запасом.
Здесь полный код:
using System;
using System.Diagnostics;
using System.Text;
public class StringBuilderTest
{
static void Append(string string1, string string2)
{
StringBuilder sb = new StringBuilder();
sb.Append(string1);
sb.Append("----");
sb.Append(string2);
}
static void AppendWithCapacity(string string1, string string2)
{
int capacity = string1.Length + string2.Length + 4;
StringBuilder sb = new StringBuilder(capacity);
sb.Append(string1);
sb.Append("----");
sb.Append(string2);
}
static void AppendFormat(string string1, string string2)
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0}----{1}", string1, string2);
}
static void Main(string[] args)
{
int size = int.Parse(args[0]);
int iterations = int.Parse(args[1]);
string method = args[2];
Action<string,string> action;
switch (method)
{
case "Append": action = Append; break;
case "AppendWithCapacity": action = AppendWithCapacity; break;
case "AppendFormat": action = AppendFormat; break;
default: throw new ArgumentException();
}
string string1 = new string('x', size);
string string2 = new string('y', size);
// Make sure it JITted
action(string1, string2);
GC.Collect();
Stopwatch sw = Stopwatch.StartNew();
for (int i=0; i < iterations; i++)
{
action(string1, string2);
}
sw.Stop();
Console.WriteLine("Time: {0}ms", (int) sw.ElapsedMilliseconds);
}
}
Ответ 4
Append
будет быстрее в большинстве случаев, потому что есть много перегрузок этого метода, которые позволяют компилятору вызывать правильный метод. Поскольку вы используете Strings
, StringBuilder
может использовать перегрузку String
для Append
.
AppendFormat
принимает String
, а затем Object[]
, что означает, что формат должен быть проанализирован, а каждый Object
в массиве должно быть ToString'd
, прежде чем его можно будет добавить в внутренний массив StringBuilder's
.
Примечание.. Для точки casperOne трудно дать точный ответ без дополнительных данных.
Ответ 5
StringBuilder
также имеет каскадные добавления: Append()
возвращает сам StringBuilder
, поэтому вы можете написать свой код следующим образом:
StringBuilder sb = new StringBuilder();
sb.Append(string1)
.Append("----")
.Append(string2);
Очистите, и он генерирует меньше IL-кода (хотя это действительно микро-оптимизация).
Ответ 6
Конечно, профиль должен знать наверняка в каждом случае.
Тем не менее, я думаю, что в целом он будет первым, потому что вы не будете повторно разбирать строку формата.
Однако разница была бы очень малой. В любом случае, вы действительно должны использовать AppendFormat
в большинстве случаев.
Ответ 7
Я бы предположил, что это был вызов, который сделал наименьшее количество работы. Добавляет только конкатенации строк, где AppendFormat выполняет строковые замены. Конечно, в эти дни вы никогда не сможете сказать...
Ответ 8
1 должен быть быстрее, потому что он просто добавляет строки, тогда как 2 должен создать строку на основе формата и затем добавить строку. Итак, там есть дополнительный шаг.
Ответ 9
Скорее всего 1 в вашем случае, но это не справедливое сравнение. Вы должны спросить StringBuilder.AppendFormat() vs StringBuilder.Append(string.Format()), где первый из них быстрее из-за внутренней работы с массивом char.
Однако ваш второй вариант более читабельен.