У меня есть вопрос, связанный с производительностью, относительно использования StringBuilder.
В очень длинном цикле я манипулирую StringBuilder и передаю его другому методу, например:
for (loop condition) {
StringBuilder sb = new StringBuilder();
sb.append("some string");
. . .
sb.append(anotherString);
. . .
passToMethod(sb.toString());
}
Является ли создание экземпляра StringBuilder в каждом цикле цикла хорошим решением? И лучше ли вместо этого вызывать удаление, как показано ниже?
StringBuilder sb = new StringBuilder();
for (loop condition) {
sb.delete(0, sb.length);
sb.append("some string");
. . .
sb.append(anotherString);
. . .
passToMethod(sb.toString());
}




Современная JVM действительно умна в подобных вещах. Я бы не стал сомневаться в этом и делать что-то хакерское, менее удобное для обслуживания / чтения ... если только вы не проведете надлежащие тесты производительности с производственными данными, которые подтвердят нетривиальное улучшение производительности (и задокументируете это;)
См. Тест в моем ответе ниже. Второй способ более быстрый.
@Epaga: Ваш тест мало что говорит об улучшении производительности в реальном приложении, где время, затрачиваемое на выделение StringBuilder, может быть тривиальным по сравнению с остальной частью цикла. Вот почему контекст важен при сравнительном анализе.
@Jon Я понимаю, но я предполагаю, что если весь его вопрос направлен на то, какой из них имеет более высокую производительность, то разница в 25-50% важна и что эта часть его кода будет вызываться много раз.
@Epaga: Пока он не измерил это с помощью своего реального кода, мы не сможем понять, насколько это важно. Если для каждой итерации цикла будет много кода, я сильно подозреваю, что это все равно не имеет значения. Мы не знаем, что в "..."
(Не поймите меня неправильно, кстати, ваши результаты тестов все еще очень интересны сами по себе. Я очарован микробенчмарками. Мне просто не нравится искажать свой код перед выполнением реальных тестов.)
мудрые слова, я думаю, мы оба полностью согласны. :-)
В соответствии с философией написания надежного кода всегда лучше помещать StringBuilder в свой цикл. Таким образом, он не выходит за рамки кода, для которого он предназначен.
Во-вторых, самое большое улучшение в StringBuilder заключается в том, что ему был задан начальный размер, чтобы он не увеличивался во время выполнения цикла.
for (loop condition) {
StringBuilder sb = new StringBuilder(4096);
}
Вы всегда можете охватить все это фигурными скобками, чтобы у вас не было Stringbuilder снаружи.
@Epaga: Это все еще вне цикла. Да, это не загрязняет внешнюю область видимости, но это неестественный способ написать код для улучшения производительности, который не был проверен в контексте.
Или, что еще лучше, поместите все в отдельный метод. ;-) Но я слышал, что ты: контекст.
Еще лучше инициализировать с ожидаемым размером вместо произвольного числа суммы (4096). Ваш код может возвращать String, который ссылается на char [] размера 4096 (зависит от JDK; насколько я помню, это было в случае с 1.4)
Основываясь на моем опыте разработки программного обеспечения в Windows, я бы сказал, что очистка StringBuilder во время вашего цикла дает лучшую производительность, чем создание экземпляра StringBuilder на каждой итерации. Его очистка освобождает эту память для немедленной перезаписи без дополнительных выделений. Я недостаточно знаком с сборщиком мусора Java, но я думаю, что освобождение и отсутствие перераспределения (если ваша следующая строка не увеличивает StringBuilder) более выгодно, чем создание экземпляра.
(Мое мнение противоречит тому, что предлагают все остальные. Хм. Пора проверить это.)
Дело в том, что в любом случае необходимо перераспределить больше памяти, поскольку существующие данные используются вновь созданной строкой в конце предыдущей итерации цикла.
О, это имеет смысл, хотя у меня было то, что toString выделяла и возвращала новый экземпляр строки, а байтовый буфер для построителя очищался вместо повторного выделения.
Тест Epaga показывает, что очистка и повторное использование - это преимущество над созданием экземпляров на каждом проходе.
Второй примерно на 25% быстрее в моем мини-тесте.
public class ScratchPad {
static String a;
public static void main( String[] args ) throws Exception {
long time = System.currentTimeMillis();
for( int i = 0; i < 10000000; i++ ) {
StringBuilder sb = new StringBuilder();
sb.append( "someString" );
sb.append( "someString2"+i );
sb.append( "someStrin4g"+i );
sb.append( "someStr5ing"+i );
sb.append( "someSt7ring"+i );
a = sb.toString();
}
System.out.println( System.currentTimeMillis()-time );
time = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for( int i = 0; i < 10000000; i++ ) {
sb.delete( 0, sb.length() );
sb.append( "someString" );
sb.append( "someString2"+i );
sb.append( "someStrin4g"+i );
sb.append( "someStr5ing"+i );
sb.append( "someSt7ring"+i );
a = sb.toString();
}
System.out.println( System.currentTimeMillis()-time );
}
}
Полученные результаты:
25265
17969
Обратите внимание, что это с JRE 1.6.0_07.
Основываясь на идеях Джона Скита в редакции, вот версия 2. Тем не менее, результаты те же.
public class ScratchPad {
static String a;
public static void main( String[] args ) throws Exception {
long time = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for( int i = 0; i < 10000000; i++ ) {
sb.delete( 0, sb.length() );
sb.append( "someString" );
sb.append( "someString2" );
sb.append( "someStrin4g" );
sb.append( "someStr5ing" );
sb.append( "someSt7ring" );
a = sb.toString();
}
System.out.println( System.currentTimeMillis()-time );
time = System.currentTimeMillis();
for( int i = 0; i < 10000000; i++ ) {
StringBuilder sb2 = new StringBuilder();
sb2.append( "someString" );
sb2.append( "someString2" );
sb2.append( "someStrin4g" );
sb2.append( "someStr5ing" );
sb2.append( "someSt7ring" );
a = sb2.toString();
}
System.out.println( System.currentTimeMillis()-time );
}
}
Полученные результаты:
5016
7516
Я добавил правку в свой ответ, чтобы объяснить, почему это может происходить. Я посмотрю более внимательно через некоторое время (45 минут). Обратите внимание, что выполнение конкатенации в вызовах добавления несколько снижает смысл использования StringBuilder в первую очередь :)
Также было бы интересно посмотреть, что произойдет, если вы поменяете местами два блока - JIT все еще «разогревает» StringBuilder во время первого теста. Это может быть неактуально, но попробовать интересно.
Я бы все равно выбрал первую версию, потому что это очиститель. Но хорошо, что вы действительно выполнили тест :) Следующее предлагаемое изменение: попробуйте №1 с соответствующей емкостью, переданной в конструктор.
Проверено, запуск этого теста с перевернутыми тестами и несколько раз подряд приводит к существенному увеличению производительности (3129 мс с перераспределением по сравнению с 5903 мс для создания экземпляра) после удаления конкатенации.
Также с 1024 для конструктора и увеличением операций добавления для выполнения примерно 1024 символов (меньше 1024, поэтому дополнительное выделение не требуется) - это перераспределение 5264 мс по сравнению с экземпляром 13985 мс.
Я бы хотел, чтобы он работал с производственными данными OP. Есть ли у него / нее 200 приложений? Струны действительно большие? Как это повлияет на тест?
В конце концов, это 10 миллионов итераций и экземпляров; в какой момент распределение проигрывает созданию экземпляра?
Используйте sb.setLength (0); вместо этого это самый быстрый способ очистить содержимое StringBuilder от воссоздания объекта или использования .delete (). Обратите внимание, что это не относится к StringBuffer, его проверки параллелизма сводят на нет преимущество в скорости.
Неэффективный ответ. П. Аррайя и Дэйв Джарвис правы. setLength (0) - безусловно, самый эффективный ответ. StringBuilder поддерживается массивом символов и является изменяемым. В момент вызова .toString () массив символов копируется и используется для поддержки неизменяемой строки. На этом этапе изменяемый буфер StringBuilder можно повторно использовать, просто переместив указатель вставки обратно в ноль (через .setLength (0)). sb.toString создает еще одну копию (неизменяемый массив символов), поэтому для каждой итерации требуется два буфера, в отличие от метода .setLength (0), который требует только один новый буфер на цикл.
В моем тестовом примере с полмиллионом итераций и тестированием как с delete, так и с setLength, цикл удаления каждый раз опережает setLength. @Chris, ваш комментарий имеет смысл, но результаты говорят об обратном с моей стороны.
Хорошо, теперь я понимаю, что происходит, и это имеет смысл.
У меня создалось впечатление, что toString просто передал базовый char[] в конструктор String, который не сделал принимает копию. Затем копия будет сделана при следующей операции «записи» (например, delete). Я считаю, что это был случай с StringBuffer в какой-то предыдущей версии. (Сейчас это не так.) Но нет - toString просто передает массив (а также индекс и длину) общедоступному конструктору String, который принимает копию.
Таким образом, в случае «повторного использования StringBuilder» мы действительно создаем одну копию данных для каждой строки, все время используя один и тот же массив символов в буфере. Очевидно, что создание нового StringBuilder каждый раз создает новый базовый буфер, а затем этот буфер копируется (несколько бессмысленно, в нашем конкретном случае, но делается по соображениям безопасности) при создании новой строки.
Все это приводит к тому, что вторая версия определенно более эффективна, но в то же время я бы сказал, что это более уродливый код.
Просто забавная информация о .NET, там ситуация другая. .NET StringBuilder внутренне изменяет обычный «строковый» объект, а метод toString просто возвращает его (помечая его как немодифицируемый, поэтому последующие манипуляции с StringBuilder будут воссоздавать его). Таким образом, типичная последовательность «новый StringBuilder-> модифицируйте его-> в String» не будет делать никаких дополнительных копий (только для расширения хранилища или его сжатия, если результирующая длина строки намного меньше, чем ее емкость). В Java этот цикл всегда создает хотя бы одну копию (в StringBuilder.toString ()).
В Sun JDK до 1.5 была оптимизация, которую вы предполагали: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959
Объявить один раз и назначить каждый раз. Это более прагматичная и многоразовая концепция, чем оптимизация.
Первый лучше для людей. Если второй работает немного быстрее на некоторых версиях некоторых JVM, что с того?
Если производительность настолько критична, обойдите StringBuilder и напишите свой собственный. Если вы хороший программист и примете во внимание, как ваше приложение использует эту функцию, вы сможете сделать это еще быстрее. Стоит? Возможно нет.
Почему этот вопрос обозначен как «любимый вопрос»? Потому что оптимизация производительности - это очень весело, независимо от того, практично это или нет.
Это не только академический вопрос. Хотя в большинстве случаев (читай 95%) я предпочитаю удобочитаемость и удобство обслуживания, на самом деле есть случаи, когда небольшие улучшения имеют большое значение ...
Хорошо, я изменю свой ответ. Если объект предоставляет метод, позволяющий его очистить и повторно использовать, сделайте это. Сначала изучите код, если хотите убедиться, что очистка эффективна; может быть, он освобождает частный массив! Если это эффективно, выделите объект вне цикла и повторно используйте его внутри.
Поскольку я не думаю, что на это еще указывалось, из-за оптимизации, встроенной в компилятор Sun Java, который автоматически создает StringBuilders (StringBuffers pre-J2SE 5.0), когда видит конкатенации строк, первый пример в вопросе эквивалентен:
for (loop condition) {
String s = "some string";
. . .
s += anotherString;
. . .
passToMethod(s);
}
Что более читабельно, ИМО, тем лучше. Ваши попытки оптимизации могут привести к выигрышу для одной платформы, но потенциально к потерям для других.
Но если вы действительно сталкиваетесь с проблемами с производительностью, тогда, конечно, оптимизируйте. Я бы начал с явного указания размера буфера StringBuilder, как сказал Джон Скит.
Еще быстрее:
public class ScratchPad {
private static String a;
public static void main( String[] args ) throws Exception {
final long time = System.currentTimeMillis();
// Pre-allocate enough space to store all appended strings.
// StringBuilder, ultimately, uses an array of characters.
final StringBuilder sb = new StringBuilder( 128 );
for( int i = 0; i < 10000000; i++ ) {
// Resetting the string is faster than creating a new object.
// Since this is a critical loop, every instruction counts.
sb.setLength( 0 );
sb.append( "someString" );
sb.append( "someString2" );
sb.append( "someStrin4g" );
sb.append( "someStr5ing" );
sb.append( "someSt7ring" );
setA( sb.toString() );
}
System.out.println( System.currentTimeMillis() - time );
}
private static void setA( final String aString ) {
a = aString;
}
}
В философии написания твердого кода внутренняя работа метода скрыта от клиентских объектов. Таким образом, с точки зрения системы не имеет значения, повторно объявляете ли вы StringBuilder внутри цикла или вне цикла. Поскольку объявление его вне цикла происходит быстрее и не усложняет код значительно, используйте объект повторно.
Даже если это было намного сложнее, и вы наверняка знали, что создание экземпляров объекта является узким местом, прокомментируйте это.
Три прогона с этим ответом:
$ java ScratchPad
1567
$ java ScratchPad
1569
$ java ScratchPad
1570
Три пробега с другим ответом:
$ java ScratchPad2
1663
2231
$ java ScratchPad2
1656
2233
$ java ScratchPad2
1658
2242
Хотя это несущественно, установка начального размера буфера StringBuilder для предотвращения перераспределения памяти даст небольшой прирост производительности.
Это, безусловно, лучший ответ. StringBuilder поддерживается массивом символов и является изменяемым. В момент вызова .toString () массив символов копируется и используется для поддержки неизменяемой строки. На этом этапе изменяемый буфер StringBuilder можно повторно использовать, просто переместив указатель вставки обратно в ноль (через .setLength (0)). Эти ответы, предлагающие выделить новый StringBuilder для каждого цикла, похоже, не понимают, что .toString создает еще одну копию, поэтому для каждой итерации требуется два буфера в отличие от метода .setLength (0), который требует только одного нового буфера на цикл.
Причина, по которой выполнение setLength или delete улучшает производительность, в основном заключается в том, что код «изучает» правильный размер буфера, а не в распределении памяти. Обычно Я рекомендую позволить компилятору выполнить оптимизацию строк. Однако, если производительность критична, я часто заранее рассчитываю ожидаемый размер буфера. Размер StringBuilder по умолчанию составляет 16 символов. Если вы вырастете за пределы этого, его размер придется изменить. Изменение размера - вот где теряется производительность. Вот еще один мини-тест, который это иллюстрирует:
private void clear() throws Exception {
long time = System.currentTimeMillis();
int maxLength = 0;
StringBuilder sb = new StringBuilder();
for( int i = 0; i < 10000000; i++ ) {
// Resetting the string is faster than creating a new object.
// Since this is a critical loop, every instruction counts.
//
sb.setLength( 0 );
sb.append( "someString" );
sb.append( "someString2" ).append( i );
sb.append( "someStrin4g" ).append( i );
sb.append( "someStr5ing" ).append( i );
sb.append( "someSt7ring" ).append( i );
maxLength = Math.max(maxLength, sb.toString().length());
}
System.out.println(maxLength);
System.out.println("Clear buffer: " + (System.currentTimeMillis()-time) );
}
private void preAllocate() throws Exception {
long time = System.currentTimeMillis();
int maxLength = 0;
for( int i = 0; i < 10000000; i++ ) {
StringBuilder sb = new StringBuilder(82);
sb.append( "someString" );
sb.append( "someString2" ).append( i );
sb.append( "someStrin4g" ).append( i );
sb.append( "someStr5ing" ).append( i );
sb.append( "someSt7ring" ).append( i );
maxLength = Math.max(maxLength, sb.toString().length());
}
System.out.println(maxLength);
System.out.println("Pre allocate: " + (System.currentTimeMillis()-time) );
}
public void testBoth() throws Exception {
for(int i = 0; i < 5; i++) {
clear();
preAllocate();
}
}
Результаты показывают, что повторное использование объекта примерно на 10% быстрее, чем создание буфера ожидаемого размера.
LOL, я впервые увидел, как люди сравнивают производительность, комбинируя строку в StringBuilder. Для этой цели, если вы используете "+", это может быть еще быстрее; D. Цель использования StringBuilder для ускорения извлечения всей строки как концепции «локальности».
В сценарии, в котором вы часто извлекаете значение String, которое не требует частого изменения, Stringbuilder обеспечивает более высокую производительность извлечения строки. И это цель использования StringBuilder .. пожалуйста, не тестируйте MIS-Test его основную цель ..
Некоторые говорили: «Самолет летит быстрее». Поэтому я протестировал это на своем байке и обнаружил, что самолет движется медленнее. Вы знаете, как я выставляю настройки эксперимента? D
Не значительно быстрее, но из моих тестов показывает, что в среднем на пару миллисек быстрее при использовании 1.6.0_45 64 бит: используйте StringBuilder.setLength (0) вместо StringBuilder.delete ():
time = System.currentTimeMillis();
StringBuilder sb2 = new StringBuilder();
for (int i = 0; i < 10000000; i++) {
sb2.append( "someString" );
sb2.append( "someString2"+i );
sb2.append( "someStrin4g"+i );
sb2.append( "someStr5ing"+i );
sb2.append( "someSt7ring"+i );
a = sb2.toString();
sb2.setLength(0);
}
System.out.println( System.currentTimeMillis()-time );
Самый быстрый способ - использовать setLength. Это не будет связано с операцией копирования. Способ создания нового StringBuilder должен быть полностью исключен. Замедление для StringBuilder.delete (int start, int end) связано с тем, что он снова скопирует массив для части изменения размера.
System.arraycopy(value, start+len, value, start, count-end);
После этого StringBuilder.delete () обновит StringBuilder.count до нового размера. В то время как StringBuilder.setLength () просто упрощает обновление StringBuilder.count до нового размера.
Я не думаю, что имеет смысл пытаться таким образом оптимизировать производительность. Сегодня (2019 г.) оба состояния работают около 11 секунд для 100 000 000 циклов на моем ноутбуке I5:
String a;
StringBuilder sb = new StringBuilder();
long time = 0;
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
StringBuilder sb3 = new StringBuilder();
sb3.append("someString");
sb3.append("someString2");
sb3.append("someStrin4g");
sb3.append("someStr5ing");
sb3.append("someSt7ring");
a = sb3.toString();
}
System.out.println(System.currentTimeMillis() - time);
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
sb.setLength(0);
sb.delete(0, sb.length());
sb.append("someString");
sb.append("someString2");
sb.append("someStrin4g");
sb.append("someStr5ing");
sb.append("someSt7ring");
a = sb.toString();
}
System.out.println(System.currentTimeMillis() - time);
==> 11000 мс (объявление внутри цикла) и 8236 мс (объявление вне цикла)
Даже если я запускаю программы для дедупликации адресов с несколькими миллиардами циклов разница в 2 сек. для 100 миллионов циклов не имеет никакого значения, потому что эти программы выполняются часами. Также имейте в виду, что все будет по-другому, если у вас есть только один оператор добавления:
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
StringBuilder sb3 = new StringBuilder();
sb3.append("someString");
a = sb3.toString();
}
System.out.println(System.currentTimeMillis() - time);
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
sb.setLength(0);
sb.delete(0, sb.length());
sb.append("someString");
a = sb.toString();
}
System.out.println(System.currentTimeMillis() - time);
==> 3416 мс (внутренний цикл), 3555 мс (внешний цикл) В этом случае первая инструкция, которая создает StringBuilder внутри цикла, выполняется быстрее. И, если вы измените порядок выполнения, он будет намного быстрее:
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
sb.setLength(0);
sb.delete(0, sb.length());
sb.append("someString");
a = sb.toString();
}
System.out.println(System.currentTimeMillis() - time);
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
StringBuilder sb3 = new StringBuilder();
sb3.append("someString");
a = sb3.toString();
}
System.out.println(System.currentTimeMillis() - time);
==> 3638 мс (внешний цикл), 2908 мс (внутренний цикл)
С уважением, Ульрих
Где "нетривиальность" является ключевым моментом - тесты могут показать, что одна форма пропорционально быстрее, но без намека на то, сколько времени это занимает в реальном приложении :)