Лучше ли повторно использовать StringBuilder в цикле? - PullRequest
96 голосов
/ 28 октября 2008

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

Ответы [ 14 ]

66 голосов
/ 28 октября 2008

Второй показатель примерно на 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
25 голосов
/ 28 октября 2008

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

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

for (loop condition) {
  StringBuilder sb = new StringBuilder(4096);
}
23 голосов
/ 22 июня 2009

Еще быстрее:

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

12 голосов
/ 28 октября 2008

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

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

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

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

9 голосов
/ 30 октября 2008

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

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

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

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

4 голосов
/ 28 октября 2008

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

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

4 голосов
/ 28 октября 2008

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

1 голос
/ 29 сентября 2013

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

1 голос
/ 05 июня 2013

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

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

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

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

...