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


101

У меня есть вопрос, связанный с производительностью, относительно использования StringBuilder. В очень длинном цикле я манипулирую a 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());
}

Ответы:


69

Второй примерно на 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

4
Я добавил правку в свой ответ, чтобы объяснить, почему это может происходить. Я посмотрю более внимательно через некоторое время (45 минут). Обратите внимание, что выполнение конкатенации в вызовах добавления несколько снижает смысл использования StringBuilder в первую очередь :)
Джон Скит,

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

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

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

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

25

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

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

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

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

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

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

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

24

Еще быстрее:

public class ScratchPad {

    private static String a;

    public static void main( String[] args ) throws Exception {
        long time = System.currentTimeMillis();
        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( 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начального размера буфера даст небольшой выигрыш.


3
Это, безусловно, лучший ответ. StringBuilder поддерживается массивом символов и является изменяемым. В момент вызова .toString () массив символов копируется и используется для поддержки неизменяемой строки. На этом этапе изменяемый буфер StringBuilder можно повторно использовать, просто переместив указатель вставки обратно в ноль (через .setLength (0)). Эти ответы, предлагающие выделить новый StringBuilder для каждого цикла, похоже, не понимают, что .toString создает еще одну копию, поэтому для каждой итерации требуется два буфера, в отличие от метода .setLength (0), который требует только одного нового буфера на цикл.
Крис

12

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

У меня создалось впечатление, что я 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
Дэн Бериндей

9

Поскольку я не думаю, что на это еще указывалось, из-за оптимизации, встроенной в компилятор Sun Java, который автоматически создает StringBuilders (StringBuffers pre-J2SE 5.0), когда видит конкатенации строк, первый пример в вопросе эквивалентен:

for (loop condition) {
  String s = "some string";
  . . .
  s += anotherString;
  . . .
  passToMethod(s);
}

Что более читабельно, ИМО, тем лучше. Ваши попытки оптимизации могут привести к выигрышу для одной платформы, но потенциально к потерям для других.

Но если вы действительно сталкиваетесь с проблемами с производительностью, тогда, конечно, оптимизируйте. Я бы начал с явного указания размера буфера StringBuilder, согласно Джону Скиту.


4

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


Там, где «нетривиальность» является ключевой - тесты могут показать, что одна форма пропорционально быстрее, но без намека на то, сколько времени это занимает в реальном приложении :)
Джон Скит

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

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

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

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

4

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

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


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

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

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

1

Причина, по которой выполнение 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% быстрее, чем создание буфера ожидаемого размера.


1

LOL, я впервые увидел, как люди сравнивают производительность, комбинируя строку в StringBuilder. Для этой цели, если вы используете "+", это может быть еще быстрее; D. Цель использования StringBuilder для ускорения извлечения всей строки как понятия «локальность».

В сценарии, когда вы часто извлекаете значение String, которое не требует частого изменения, Stringbuilder обеспечивает более высокую производительность извлечения строки. И это цель использования StringBuilder .. пожалуйста, не тестируйте MIS-Test его основную цель ..

Некоторые говорили: «Самолет летит быстрее». Поэтому я протестировал это на своем байке и обнаружил, что самолет движется медленнее. Вы знаете, как я устанавливаю настройки эксперимента? D


1

Не значительно быстрее, но из моих тестов показывает, что в среднем на пару миллисек быстрее при использовании 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 );

1

Самый быстрый способ - использовать "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 до нового размера.


0

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

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

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


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

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

0

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

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


-2

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.