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

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

for (loop condition) {
    StringBuilder sb = new StringBuilder();
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}

Является ли создание экземпляра StringBuilder в каждом цикле цикла хорошим решением? И вместо этого лучше удалить delete, например следующее:

StringBuilder sb = new StringBuilder();
for (loop condition) {
    sb.delete(0, sb.length);
    sb.append("some string");
    . . .
    sb.append(anotherString);
    . . .
    passToMethod(sb.toString());
}

Ответ 1

В моем мини-контроле второй показатель примерно на 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

Ответ 2

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

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

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

Ответ 3

Быстрее:

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 даст небольшой коэффициент усиления.

Ответ 4

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

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

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

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

Ответ 5

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

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

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

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

Ответ 6

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

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

Некоторые люди сказали, что самолет летит быстрее. Поэтому я тестирую его на своем велосипеде и обнаружил, что самолет движется медленнее. Знаете ли вы, как я установил параметры эксперимента, D

Ответ 10

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

Ответ 11

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

Ответ 12

Увы, я нахожусь на С# нормально, я думаю, что очистка StringBuilder весит больше, чем создание нового.

Ответ 13

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

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

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

Ответ 14

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