Чи краще повторно використовувати StringBuilder у циклі?


101

У мене питання щодо продуктивності щодо використання 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());
}

Відповіді:


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 підтримується масивом char та змінюється. У точці виклику .toString () масив char копіюється і використовується для резервного копіювання незмінного рядка. На цьому етапі змінений буфер StringBuilder можна повторно використовувати, просто перемістивши вказівник вставки назад до нуля (через .setLength (0)). sb.toString створює ще одну копію (незмінний масив char), тому для кожної ітерації потрібні два буфери на відміну від методу .setLength (0), який вимагає лише одного нового буфера на цикл.
Кріс

25

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

По-друге, найбільша поліпшення в StringBuilder виходить від надання йому початкового розміру, щоб уникнути його збільшення, поки цикл працює

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

1
Ви завжди можете зафіксувати всю справу фігурними дужками, тому у вас немає Stringbuilder зовні.
Епага

@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 підтримується масивом char та змінюється. У точці виклику .toString () масив char копіюється і використовується для резервного копіювання незмінного рядка. На цьому етапі змінений буфер StringBuilder можна повторно використовувати, просто перемістивши вказівник вставки назад до нуля (через .setLength (0)). Ті відповіді, що пропонують виділити абсолютно новий StringBuilder за циклом, схоже, не усвідомлюють, що .toString створює ще одну копію, тому для кожної ітерації потрібні два буфери на відміну від методу .setLength (0), який вимагає лише одного нового буфера на цикл.
Кріс

12

Гаразд, я зараз розумію, що відбувається, і це має сенс.

У мене було враження, що toStringякраз перейшов основу char[]в конструктор String, який не взяв копію. Потім буде зроблена копія при наступній операції "запис" (наприклад delete). Я вважаю, що так було і StringBufferв попередній версії. (Це не зараз.) Але ні - toStringпросто передає масив (та індекс та довжину) громадському Stringконструктору, який бере копію.

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

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


Просто якась смішна інформація про .NET, ситуація інша. .NET StringBuilder внутрішньо модифікує звичайний "рядковий" об'єкт, а метод toString просто повертає його (позначаючи його як неможливий для зміни, тому послідовні маніпуляції зі StringBuilder відновлять його знову). Отже, типова послідовність "new StringBuilder-> modify it-> to String" не буде робити жодної додаткової копії (лише для розширення сховища або його скорочення, якщо отримана довжина рядка набагато коротша, ніж ємність). У Java цей цикл завжди робить принаймні одну копію (у StringBuilder.toString ()).
Іван Дубров

Sun JDK pre-1.5 провів оптимізацію, яку ви припускали: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6219959
Dan Berindei

9

Оскільки я не думаю, що це ще не було зазначено, через оптимізації, вбудовані в компілятор Sun Java, який автоматично створює StringBuilders (StringBuffers pre-J2SE 5.0), коли він бачить об'єднання String, перший приклад у питанні еквівалентний:

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

Що легше читати, ІМО, тим краще підходити. Ваші спроби оптимізації можуть призвести до виграшів на одній платформі, але потенційно можуть втратити інші.

Але якщо ви справді стикаєтеся з проблемами з продуктивністю, то впевнено оптимізуйте. Я б почав із чіткого визначення розміру буфера StringBuilder, за Jon Skeet.


4

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


Де ключове значення "нетривіальне" - тести можуть показувати, що одна форма пропорційно швидша, але без підказки про те, скільки часу займає реальна програма :)
Джон Скіт,

Дивіться орієнтир у моїй відповіді нижче. Другий шлях швидший.
Епага

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

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

1
(Не зрозумійте мене неправильно, btw - результати ваших орієнтирів все ще дуже цікаві самі по собі. Мене захоплюють мікро-показники. Мені просто не подобається згинати код з форми перед тим, як виконувати тести реального життя.)
Джон Скіт

4

Виходячи з мого досвіду розробки програмного забезпечення в Windows, я б сказав, що очищення StringBuilder під час циклу має кращу ефективність, ніж інстанціювання StringBuilder з кожною ітерацією. Після її очищення пам'ять потрібно негайно перезаписати, не потребуючи додаткового виділення. Я недостатньо знайомий зі збирачем сміття Java, але думаю, що звільнення та перерозподіл (якщо ваша наступна рядок не зростає StringBuilder) є більш корисним, ніж інстанція.

(Моя думка суперечить тому, що всі інші пропонують. Хм. Час орієнтуватися на це.)


Вся справа в тому, що все-таки потрібно перерозподілити більше пам'яті, оскільки існуючі дані використовуються новоствореною String в кінці попередньої ітерації циклу.
Джон Скіт

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

Орієнтовний показник Epaga показує, що очищення та повторне використання - це виграш від екземпляра при кожному проході.
cfeduke

1

Причиною, чому виконання "setLength" або "видалення" покращує продуктивність, здебільшого є кодом "навчання" правильного розміру буфера, а менше - для розподілу пам'яті. Як правило, я рекомендую дозволити компілятору робити оптимізацію рядків . Однак якщо продуктивність є критичною, я часто заздалегідь обчислюю очікуваний розмір буфера. Стандартний розмір 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-тестуйте основну мету цього ..

Деякі люди казали: Літак летить швидше. Тому я перевіряв його на своєму велосипеді, і виявив, що літак рухається повільніше. Чи знаєте ви, як я встановлюю параметри експерименту; D


1

Не значно швидше, але з моїх тестів це показує, що в середньому на пару мілісів швидше, використовуючи 64.0 біт 1.6.0_45: використовуйте 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

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.