Лучше ли повторно использовать StringBuilder в цикле?

У меня есть вопрос, связанный с производительностью, относительно использования 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());
}
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
102
0
72 470
14

Ответы 14

Современная JVM действительно умна в подобных вещах. Я бы не стал сомневаться в этом и делать что-то хакерское, менее удобное для обслуживания / чтения ... если только вы не проведете надлежащие тесты производительности с производственными данными, которые подтвердят нетривиальное улучшение производительности (и задокументируете это;)

Где "нетривиальность" является ключевым моментом - тесты могут показать, что одна форма пропорционально быстрее, но без намека на то, сколько времени это занимает в реальном приложении :)

Jon Skeet 28.10.2008 10:28

См. Тест в моем ответе ниже. Второй способ более быстрый.

Epaga 28.10.2008 10:34

@Epaga: Ваш тест мало что говорит об улучшении производительности в реальном приложении, где время, затрачиваемое на выделение StringBuilder, может быть тривиальным по сравнению с остальной частью цикла. Вот почему контекст важен при сравнительном анализе.

Jon Skeet 28.10.2008 10:40

@Jon Я понимаю, но я предполагаю, что если весь его вопрос направлен на то, какой из них имеет более высокую производительность, то разница в 25-50% важна и что эта часть его кода будет вызываться много раз.

Epaga 28.10.2008 10:43

@Epaga: Пока он не измерил это с помощью своего реального кода, мы не сможем понять, насколько это важно. Если для каждой итерации цикла будет много кода, я сильно подозреваю, что это все равно не имеет значения. Мы не знаем, что в "..."

Jon Skeet 28.10.2008 10:52

(Не поймите меня неправильно, кстати, ваши результаты тестов все еще очень интересны сами по себе. Я очарован микробенчмарками. Мне просто не нравится искажать свой код перед выполнением реальных тестов.)

Jon Skeet 28.10.2008 10:59

мудрые слова, я думаю, мы оба полностью согласны. :-)

Epaga 28.10.2008 11:05

В соответствии с философией написания надежного кода всегда лучше помещать StringBuilder в свой цикл. Таким образом, он не выходит за рамки кода, для которого он предназначен.

Во-вторых, самое большое улучшение в StringBuilder заключается в том, что ему был задан начальный размер, чтобы он не увеличивался во время выполнения цикла.

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}

Вы всегда можете охватить все это фигурными скобками, чтобы у вас не было Stringbuilder снаружи.

Epaga 28.10.2008 10:37

@Epaga: Это все еще вне цикла. Да, это не загрязняет внешнюю область видимости, но это неестественный способ написать код для улучшения производительности, который не был проверен в контексте.

Jon Skeet 28.10.2008 10:42

Или, что еще лучше, поместите все в отдельный метод. ;-) Но я слышал, что ты: контекст.

Epaga 28.10.2008 10:46

Еще лучше инициализировать с ожидаемым размером вместо произвольного числа суммы (4096). Ваш код может возвращать String, который ссылается на char [] размера 4096 (зависит от JDK; насколько я помню, это было в случае с 1.4)

kohlerm 28.10.2008 10:53

Основываясь на моем опыте разработки программного обеспечения в Windows, я бы сказал, что очистка StringBuilder во время вашего цикла дает лучшую производительность, чем создание экземпляра StringBuilder на каждой итерации. Его очистка освобождает эту память для немедленной перезаписи без дополнительных выделений. Я недостаточно знаком с сборщиком мусора Java, но я думаю, что освобождение и отсутствие перераспределения (если ваша следующая строка не увеличивает StringBuilder) более выгодно, чем создание экземпляра.

(Мое мнение противоречит тому, что предлагают все остальные. Хм. Пора проверить это.)

Дело в том, что в любом случае необходимо перераспределить больше памяти, поскольку существующие данные используются вновь созданной строкой в ​​конце предыдущей итерации цикла.

Jon Skeet 28.10.2008 10:19

О, это имеет смысл, хотя у меня было то, что toString выделяла и возвращала новый экземпляр строки, а байтовый буфер для построителя очищался вместо повторного выделения.

cfeduke 28.10.2008 10:27

Тест Epaga показывает, что очистка и повторное использование - это преимущество над созданием экземпляров на каждом проходе.

cfeduke 28.10.2008 10:43

Второй примерно на 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 в первую очередь :)

Jon Skeet 28.10.2008 10:26

Также было бы интересно посмотреть, что произойдет, если вы поменяете местами два блока - JIT все еще «разогревает» StringBuilder во время первого теста. Это может быть неактуально, но попробовать интересно.

Jon Skeet 28.10.2008 10:27

Я бы все равно выбрал первую версию, потому что это очиститель. Но хорошо, что вы действительно выполнили тест :) Следующее предлагаемое изменение: попробуйте №1 с соответствующей емкостью, переданной в конструктор.

Jon Skeet 28.10.2008 10:35

Проверено, запуск этого теста с перевернутыми тестами и несколько раз подряд приводит к существенному увеличению производительности (3129 мс с перераспределением по сравнению с 5903 мс для создания экземпляра) после удаления конкатенации.

cfeduke 28.10.2008 10:38

Также с 1024 для конструктора и увеличением операций добавления для выполнения примерно 1024 символов (меньше 1024, поэтому дополнительное выделение не требуется) - это перераспределение 5264 мс по сравнению с экземпляром 13985 мс.

cfeduke 28.10.2008 10:45

Я бы хотел, чтобы он работал с производственными данными OP. Есть ли у него / нее 200 приложений? Струны действительно большие? Как это повлияет на тест?

Stu Thompson 28.10.2008 10:53

В конце концов, это 10 миллионов итераций и экземпляров; в какой момент распределение проигрывает созданию экземпляра?

cfeduke 28.10.2008 18:09

Используйте sb.setLength (0); вместо этого это самый быстрый способ очистить содержимое StringBuilder от воссоздания объекта или использования .delete (). Обратите внимание, что это не относится к StringBuffer, его проверки параллелизма сводят на нет преимущество в скорости.

P Arrayah 12.11.2008 00:00

Неэффективный ответ. П. Аррайя и Дэйв Джарвис правы. setLength (0) - безусловно, самый эффективный ответ. StringBuilder поддерживается массивом символов и является изменяемым. В момент вызова .toString () массив символов копируется и используется для поддержки неизменяемой строки. На этом этапе изменяемый буфер StringBuilder можно повторно использовать, просто переместив указатель вставки обратно в ноль (через .setLength (0)). sb.toString создает еще одну копию (неизменяемый массив символов), поэтому для каждой итерации требуется два буфера, в отличие от метода .setLength (0), который требует только один новый буфер на цикл.

Chris 01.08.2015 20:40

В моем тестовом примере с полмиллионом итераций и тестированием как с delete, так и с setLength, цикл удаления каждый раз опережает setLength. @Chris, ваш комментарий имеет смысл, но результаты говорят об обратном с моей стороны.

uchamp 11.09.2017 15:22

Хорошо, теперь я понимаю, что происходит, и это имеет смысл.

У меня создалось впечатление, что toString просто передал базовый char[] в конструктор String, который не сделал принимает копию. Затем копия будет сделана при следующей операции «записи» (например, delete). Я считаю, что это был случай с StringBuffer в какой-то предыдущей версии. (Сейчас это не так.) Но нет - toString просто передает массив (а также индекс и длину) общедоступному конструктору String, который принимает копию.

Таким образом, в случае «повторного использования StringBuilder» мы действительно создаем одну копию данных для каждой строки, все время используя один и тот же массив символов в буфере. Очевидно, что создание нового StringBuilder каждый раз создает новый базовый буфер, а затем этот буфер копируется (несколько бессмысленно, в нашем конкретном случае, но делается по соображениям безопасности) при создании новой строки.

Все это приводит к тому, что вторая версия определенно более эффективна, но в то же время я бы сказал, что это более уродливый код.

Просто забавная информация о .NET, там ситуация другая. .NET StringBuilder внутренне изменяет обычный «строковый» объект, а метод toString просто возвращает его (помечая его как немодифицируемый, поэтому последующие манипуляции с StringBuilder будут воссоздавать его). Таким образом, типичная последовательность «новый StringBuilder-> модифицируйте его-> в String» не будет делать никаких дополнительных копий (только для расширения хранилища или его сжатия, если результирующая длина строки намного меньше, чем ее емкость). В Java этот цикл всегда создает хотя бы одну копию (в StringBuilder.toString ()).

Ivan Dubrov 16.07.2009 05:54

В Sun JDK до 1.5 была оптимизация, которую вы предполагали: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959

Dan Berindei 01.05.2011 16:42

Объявить один раз и назначить каждый раз. Это более прагматичная и многоразовая концепция, чем оптимизация.

Первый лучше для людей. Если второй работает немного быстрее на некоторых версиях некоторых JVM, что с того?

Если производительность настолько критична, обойдите StringBuilder и напишите свой собственный. Если вы хороший программист и примете во внимание, как ваше приложение использует эту функцию, вы сможете сделать это еще быстрее. Стоит? Возможно нет.

Почему этот вопрос обозначен как «любимый вопрос»? Потому что оптимизация производительности - это очень весело, независимо от того, практично это или нет.

Это не только академический вопрос. Хотя в большинстве случаев (читай 95%) я предпочитаю удобочитаемость и удобство обслуживания, на самом деле есть случаи, когда небольшие улучшения имеют большое значение ...

Pier Luigi 30.10.2008 16:08

Хорошо, я изменю свой ответ. Если объект предоставляет метод, позволяющий его очистить и повторно использовать, сделайте это. Сначала изучите код, если хотите убедиться, что очистка эффективна; может быть, он освобождает частный массив! Если это эффективно, выделите объект вне цикла и повторно используйте его внутри.

dongilmore 02.11.2008 20:23

Поскольку я не думаю, что на это еще указывалось, из-за оптимизации, встроенной в компилятор 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), который требует только одного нового буфера на цикл.

Chris 01.08.2015 20:37

Причина, по которой выполнение 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 мс (внутренний цикл)

С уважением, Ульрих

Другие вопросы по теме