Чи потрібно використовувати Java String.format (), якщо продуктивність важлива?


215

Ми повинні будувати рядки весь час для виведення журналу тощо. У версії JDK ми дізналися, коли використовувати StringBuffer(багато додатків, безпечно для потоків) та StringBuilder(багато додатків, не захищених від потоку).

Яка порада щодо використання String.format()? Це ефективно чи ми змушені дотримуватися конкатенації для однокласників, де важлива продуктивність?

наприклад, потворний старий стиль,

String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";

порівняно з охайним новим стилем (String.format, можливо, повільніше),

String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);

Примітка: мій конкретний випадок використання - це сотні рядків журналу 'one-line' у всьому моєму коді. Вони не включають петлю, тому StringBuilderце занадто важка вага. Мене String.format()конкретно цікавить .


28
Чому б не випробувати його?
Ред С.

1
Якщо ви виробляєте цей результат, то я припускаю, що людина має читати його, оскільки людина може його прочитати. Давайте скажемо не більше 10 рядків на секунду. Я думаю, що ви знайдете, що це дійсно не має значення, який підхід ви приймете, якщо він навмисно повільніше, користувач може це оцінити. ;) Отже, ні, StringBuilder не є важкою вагою в більшості ситуацій.
Пітер Лорі

9
@ Петер, ні, це абсолютно не для читання в реальному часі людьми! Це там, щоб допомогти аналізу, коли справи йдуть не так. Вихід журналу, як правило, становить тисячі рядків в секунду, тому він повинен бути ефективним.
Повітря

5
якщо ви створюєте багато тисяч рядків в секунду, я б запропонував 1) використовувати коротший текст, навіть жоден текст, такий як звичайний CSV або двійковий файл 2) взагалі не використовуйте String, ви можете записувати дані в ByteBuffer, не створюючи будь-які об'єкти (як текстові чи двійкові) 3) підтримують запис даних на диск або сокет. Ви повинні мати можливість підтримувати близько 1 мільйона ліній в секунду. (В основному стільки, скільки дозволить ваша дискова підсистема) Ви можете досягти пакетів у 10 разів.
Пітер Лорі

7
Це не стосується загального випадку, але зокрема для ведення журналів, LogBack (написаний оригінальним автором Log4j) має форму параметризованого журналу, що вирішує цю точну проблему - logback.qos.ch/manual/architecture.html#ParametrizedLogging
Метт Пассел

Відповіді:


123

Я написав невеликий клас для тестування, який має кращі показники двох і + випереджає формат. коефіцієнтом від 5 до 6. Спробуйте самі

import java.io.*;
import java.util.Date;

public class StringTest{

    public static void main( String[] args ){
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;

    for( i = 0; i< 100000; i++){
        String s = "Blah" + i + "Blah";
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<100000; i++){
        String s = String.format("Blah %d Blah", i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    }
}

Виконання вищезазначеного для різних N показує, що обидва поводяться лінійно, але String.formatв 5-30 разів повільніше.

Причина полягає в тому, що в поточній реалізації String.formatспочатку розбирають вхід регулярними виразами, а потім заповнюють параметри. З іншого боку, об'єднання з плюсом оптимізується javac (а не JIT) та використовує StringBuilder.appendбезпосередньо.

Порівняння часу виконання


12
У цього тесту є одна вада в тому, що він не зовсім добре відображає все форматування рядків. Часто логіка бере участь у тому, що включати, і логіка для форматування конкретних значень у рядки. Будь-яка реальна перевірка повинна дивитись на реальні сценарії.
Оріон Адріан

9
У SO виникло ще одне питання про + вірші StringBuffer, в останніх версіях Java + було замінено на StringBuffer, коли це було можливо, щоб продуктивність не була іншою
hhafez

25
Це схоже на те, що мікробензик, який буде оптимізований вкрай невикористаним чином.
Девід Х. Клементс

20
Ще одна погано реалізована мікро-орієнтир. Як масштабують обидва способи за порядком. Як щодо використання, 100, 1000, 10000, 1000000, операцій. Якщо ви запускаєте лише один тест, на один порядок, у програмі, яка не працює на ізольованому ядрі; немає способу сказати, яка різниця може бути списана як «побічні ефекти» через переключення контексту, фонові процеси тощо
Еван Плейс

8
Більше того, як ви ніколи не виходите з головного JIT, не можете запустити.
Jan Zyka

241

Я взяв код hhafez і додав тест пам’яті :

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

Я запускаю це окремо для кожного підходу, оператора '+', String.format і StringBuilder (виклик toString ()), тому на використану пам'ять не впливатимуть інші підходи. Я додав більше конкатенацій, зробивши рядок як "Blah" + i + "Blah" + i + "Blah" + i + "Blah".

Результат наступний (в середньому 5 пробіжок):
Час наближення (мс)
Виділена пам'ять (довгий) оператор "+" 747 320,504
String.format 16484 373,312
StringBuilder 769 57,344

Ми можемо бачити, що String '+' і StringBuilder практично однакові за часом, але StringBuilder набагато ефективніше у використанні пам'яті. Це дуже важливо, коли у нас є багато викликів журналу (або будь-яких інших висловлювань, що включають рядки) у часовий інтервал, достатньо короткий, щоб Garbage Collector не потрапив до очищення багатьох рядкових екземплярів, що є результатом оператора "+".

І записка, до речі, не забудьте перевірити протоколювання рівня перед створенням повідомлення.

Висновки:

  1. Я продовжуватиму використовувати StringBuilder.
  2. У мене занадто багато часу або занадто мало життя.

8
"не забудьте перевірити рівень журналу перед створенням повідомлення", це хороша порада. Це потрібно зробити принаймні для повідомлень про налагодження, оскільки їх може бути багато, і вони не повинні бути включені у виробництві.
stivlo

39
Ні, це неправильно. Вибачте, що тупість, але кількість звернень, які вони залучили, нічого не викликає тривоги. За допомогою +оператора компілюється еквівалентний StringBuilderкод. Мікробензові позначки, як це, не є хорошим способом вимірювання продуктивності - чому б не використовувати jvisualvm, це в jdk чомусь. String.format() буде повільніше, але через час для розбору рядка формату, а не будь-якого розподілу об'єктів. Відкладати створення артефактів журналу до тих пір, поки не будете впевнені, що вони потрібні - це корисна порада, але якщо це вплине на продуктивність, це не в тому місці.
CurtainDog

1
@CurtainDog, ваш коментар був зроблений на чотирирічній публікації, ви можете вказати на документацію або створити окрему відповідь, щоб усунути різницю?
kurtzbot

1
Посилання на підтримку коментаря @ CurtainDog: stackoverflow.com/a/1532499/2872712 . Тобто, + є кращим, якщо це не робиться в циклі.
абрикос

And a note, BTW, don't forget to check the logging level before constructing the message.не є гарною порадою. Якщо припустити, що ми говоримо java.util.logging.*конкретно, перевірка рівня реєстрації - це коли ви говорите про те, щоб зробити розширену обробку, яка може спричинити несприятливий вплив на програму, якої ви не хотіли б, коли програма не ввімкнула реєстрацію на відповідний рівень. Форматування рядків - це не той тип обробки ВСЕ. Форматування є частиною java.util.loggingфреймворку, і сам реєстратор перевіряє рівень журналу перед тим, як форматер коли-небудь буде викликаний.
searchengine27

30

Усі представлені тут орієнтири мають деякі недоліки , тому результати не є надійними.

Я був здивований, що ніхто не використовував JMH для проведення бенчмаркінгу, так я і зробив.

Результати:

Benchmark             Mode  Cnt     Score     Error  Units
MyBenchmark.testOld  thrpt   20  9645.834 ± 238.165  ops/s  // using +
MyBenchmark.testNew  thrpt   20   429.898 ±  10.551  ops/s  // using String.format

Одиниці - це операції в секунду, чим більше, тим краще. Вихідний код орієнтиру . Була використана віртуальна машина Java OpenJDK IcedTea 2.5.4.

Отже, старий стиль (використання +) набагато швидше.


5
Це було б набагато простіше інтерпретувати, якби ви помітили, що було "+", а що "формат".
AjahnCharles

21

Ваш старий потворний стиль автоматично збирається JAVAC 1.6 як:

StringBuilder sb = new StringBuilder("What do you get if you multiply ");
sb.append(varSix);
sb.append(" by ");
sb.append(varNine);
sb.append("?");
String s =  sb.toString();

Так що різниці між цим та використанням StringBuilder абсолютно немає.

String.format набагато важкіший, оскільки він створює новий Форматтер, аналізує рядок вхідного формату, створює StringBuilder, додає все до нього і викликає toString ().


Щодо читабельності, опублікований вами код набагато більш ... громіздкий, ніж String.format ("Що ви отримуєте, якщо помножити% d на% d?", VarSix, varNine);
dusktreader

12
Немає різниці між собою +і StringBuilderсправді. На жаль, є багато дезінформації в інших відповідях у цій темі. Я майже спокусився змінити питання how should I not be measuring performance.
CurtainDog

12

String.format Java працює так:

  1. вона аналізує рядок формату, вибухаючи в список фрагментів формату
  2. він повторює фрагменти формату, перетворюючись на StringBuilder, який в основному є масивом, який змінює себе за необхідності, копіюючи в новий масив. це необхідно, тому що ми ще не знаємо, наскільки великим буде виділити остаточну рядок
  3. StringBuilder.toString () копіює свій внутрішній буфер у новий String

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

new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");

Я припускаю, що оптимізатор оптимізує обробку рядка формату. Якщо так, то вам залишається еквівалентна амортизована продуктивність для ручного розгортання вашого String.format в StringBuilder.


5
Я не думаю, що ваші міркування щодо оптимізації обробки рядків формату є правильними. У деяких реальних тестах, що використовують Java 7, я виявив, що використання String.formatвнутрішніх циклів (працює мільйони разів) призвело до більш ніж 10% мого часу виконання java.util.Formatter.parse(String). Це, мабуть, вказує на те, що у внутрішніх циклах слід уникати дзвінків Formatter.formatабо будь-чого, що викликає його, у тому числі PrintStream.format(недолік у стандартній lib Java, IMO, тим більше, що ви не можете кешувати проаналізовану рядок формату).
Енді МакКінлай

8

Щоб розгорнути / виправити першу відповідь вище, String.format не допоможе насправді.
Що String.format допоможе, це коли ви друкуєте дату / час (або числовий формат тощо), де є різниці в локалізації (l10n) (тобто деякі країни друкують 04Feb2009, а інші друкують Feb042009).
Під час перекладу ви просто говорите про переміщення будь-яких зовнішніх рядків (наприклад, повідомлень про помилки і що ні) в пакет властивостей, щоб ви могли використовувати правильний пакет для потрібної мови, використовуючи ResourceBundle та MessageFormat.

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


1
Я б сказав, що ефективність, String.format і звичайна конкатенація, зводиться до того, що вам більше подобається. Я думаю, що це неправильно. З врахуванням продуктивності, конкатенація набагато краща. Для отримання більш детальної інформації, будь ласка, подивіться на мою відповідь.
Адам Стельмащик

6

У вашому прикладі пробалбі продуктивності не надто відрізняються, але слід враховувати й інші проблеми: а саме фрагментація пам'яті. Навіть об'єднана операція - це створення нового рядка, навіть якщо його тимчасовий (для його отримання потрібен час, і це більше роботи). String.format () є просто читабельнішим і включає меншу фрагментацію.

Крім того, якщо ви багато використовуєте певний формат, не забувайте, що ви можете використовувати клас Formatter () безпосередньо (все, що String.format () робить, це створити екземпляр Formatter одноразового використання.

Крім того, слід пам’ятати про щось інше: будьте обережні, використовуючи substring (). Наприклад:

String getSmallString() {
  String largeString = // load from file; say 2M in size
  return largeString.substring(100, 300);
}

Ця велика струна все ще залишається в пам'яті, оскільки саме так працюють підряди Java. Краща версія:

  return new String(largeString.substring(100, 300));

або

  return String.format("%s", largeString.substring(100, 300));

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


8
Варто вказати на "пов'язане питання" насправді C # і, отже, не застосовується.
Повітря

який інструмент ви використовували для вимірювання фрагментації пам’яті та чи фрагментація навіть робить різницю швидкості для оперативної пам'яті?
kritzikratzi

Варто зазначити, що метод підрядки був змінений з Java 7 +. Тепер він повинен повернути нове String-представлення, що містить лише підрядкові символи. Це означає, що немає необхідності повертати дзвінок String :: new
João Rebelo

5

Як правило, ви повинні використовувати String.Format, оскільки це відносно швидко і підтримує глобалізацію (якщо ви насправді намагаєтесь написати щось, що читає користувач). Це також полегшує глобалізацію, якщо ви намагаєтеся перекласти один рядок порівняно з 3 або більше за кожне твердження (особливо для мов, що мають різну граматичну структуру).

Тепер, якщо ви ніколи не плануєте щось перекладати, то або покладайтеся на вбудовану Java для перетворення + операторів у StringBuilder. Або використовувати StringBuilderявно Java .


3

Інша перспектива лише з точки зору журналу.

Я бачу багато дискусій, пов’язаних із входом у цю нитку, тому подумав додати свій досвід у відповідь. Можливо, хтось знайде це корисним.

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

Вам не потрібно конкретизувати / форматувати, якщо ви не хочете входити в систему. Скажемо, якщо я визначу такий метод

public void logDebug(String... args, Throwable t) {
    if(debugOn) {
       // call concat methods for all args
       //log the final debug message
    }
}

У такому підході канкат / форматер насправді взагалі не викликається, якщо його повідомлення про налагодження і debugOn = false

Хоча тут все ж буде краще використовувати StringBuilder замість форматера. Основна мотивація - уникати чогось із цього.

У той же час мені не подобається додавати блок "if" для кожного запису журналу

  • Це впливає на читабельність
  • Зменшує охоплення моїх тестових одиниць - це заплутано, коли ви хочете переконатися, що кожен рядок тестується.

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


Чи можете ви використати існуючу бібліотеку на зразок slf4j-api, яка намагається вирішити цю службову скриньку за допомогою їх параметризованої функції реєстрації? slf4j.org/faq.html#logging_performance
ammianus

2

Я щойно змінив тест hhafez, щоб включити StringBuilder. StringBuilder в 33 рази швидше, ніж String.format, використовуючи клієнт jdk 1.6.0_10 на XP. Використання перемикача -server знижує коефіцієнт до 20.

public class StringTest {

   public static void main( String[] args ) {
      test();
      test();
   }

   private static void test() {
      int i = 0;
      long prev_time = System.currentTimeMillis();
      long time;

      for ( i = 0; i < 1000000; i++ ) {
         String s = "Blah" + i + "Blah";
      }
      time = System.currentTimeMillis() - prev_time;

      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         String s = String.format("Blah %d Blah", i);
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         new StringBuilder("Blah").append(i).append("Blah");
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);
   }
}

Хоча це може здатися різким, я вважаю, що це актуально лише в рідкісних випадках, оскільки абсолютна кількість досить низька: 4 с для 1 мільйона простих викликів String.format - це нормально - до тих пір, поки я використовую їх для ведення журналів або подібно до.

Оновлення: Як вказував sjbotha в коментарях, тест StringBuilder є недійсним, оскільки в ньому відсутній остаточний .toString().

Правильний коефіцієнт швидкості від String.format(.)до StringBuilder- 23 на моїй машині (16 з -serverвимикачем).


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

Я зробив це, цикл for for займає 0 мс. Але навіть якби це зайняло час, це лише посилить фактор.
the.duckman

3
Тест StringBuilder недійсний, тому що він не викликає toString () наприкінці, щоб він фактично дав вам String, який ви можете використовувати. Я додав це, і результат полягає в тому, що StringBuilder займає приблизно стільки ж часу, скільки і +. Я впевнений, що при збільшенні кількості додань це з часом стане дешевшим.
Сарел Бота

1

Ось модифікована версія запису hhafez. Він включає в себе варіант побудови рядків.

public class BLA
{
public static final String BLAH = "Blah ";
public static final String BLAH2 = " Blah";
public static final String BLAH3 = "Blah %d Blah";


public static void main(String[] args) {
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;
    int numLoops = 1000000;

    for( i = 0; i< numLoops; i++){
        String s = BLAH + i + BLAH2;
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

Час після циклу 391 Час після циклу 4163 Час після циклу 227


0

Відповідь на це дуже залежить від того, як ваш конкретний компілятор Java оптимізує байт-код, який він створює. Рядки незмінні і, теоретично, кожна операція "+" може створити нову. Але ваш компілятор майже напевно оптимізує проміжні кроки у створенні довгих рядків. Цілком можливо, що обидва рядки коду вище генерують абсолютно однаковий байт-код.

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


1
Байт-код для другого прикладу, безумовно, викликає String.format, але я б жахнувся, якби це зробила проста конкатенація. Чому компілятор використовує рядок формату, який потім доведеться розбирати?
Джон Скіт

Я використовував "байт-код", де я повинен був сказати "двійковий код". Коли все зводиться до стрибків і movs, це може бути точно таким же кодом.
Так - той Джейк.

0

Подумайте про використання "hello".concat( "world!" )для невеликої кількості рядків у конкатенації. Це може бути навіть кращим за продуктивність, ніж інші підходи.

Якщо у вас більше 3-х рядків, то розгляньте можливість використання StringBuilder або просто String, залежно від компілятора, який ви використовуєте.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.