Должен ли я использовать String.format () Java, если производительность важна?

Мы должны постоянно создавать строки для вывода журнала и так далее. В версиях JDK мы узнали, когда использовать StringBuffer (многие приложения, streamобезопасные) и StringBuilder (многие присоединяются, не streamобезопасны).

Каков совет по использованию String.format() ? Является ли это эффективным, или мы вынуждены придерживаться конкатенации для однострочных линий, где важна производительность?

например, уродливый старый стиль,

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

против аккуратного нового стиля (и, возможно, медленного),

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

Примечание: мой конкретный вариант использования – это сотни строк журнала «один-лайн» в моем коде. Они не связаны с циклом, поэтому StringBuilder слишком тяжелый. Меня интересует String.format() .

Я написал небольшой class для тестирования, который имеет лучшую производительность двух и +, опережает формат. в 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 .

Сравнение времени выполнения

Я взял код 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 намного эффективнее в использовании памяти. Это очень важно, когда у нас много вызовов журнала (или любых других операторов, содержащих строки) за короткий промежуток времени, поэтому сборщик мусора не сможет очистить множество экземпляров строк, вызванных оператором «+».

И примечание, BTW, не забудьте проверить уровень ведения журнала перед конструированием сообщения.

Выводы:

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

Ваш старый уродливый стиль автоматически компилируется 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 намного более тяжелый, поскольку он создает новый Formatter, анализирует вашу строку формата ввода, создает StringBuilder, добавляет все к нему и вызывает toString ().

Все приведенные здесь контрольные показатели имеют некоторые недостатки , поэтому результаты не являются надежными.

Я был удивлен, что никто не использовал 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 

Единицы – это операции в секунду, тем лучше. Исходный код . OpenJDK IcedTea 2.5.4 была использована виртуальная машина Java.

Таким образом, старый стиль (с использованием +) намного быстрее.

Java String.format работает так:

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

если конечным пунктом назначения для этих данных является stream (например, создание веб-страницы или запись в файл), вы можете собрать fragmentы формата непосредственно в свой stream:

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

Я предполагаю, что оптимизатор оптимизирует обработку строк формата. Если это так, вы получите эквивалентную амортизированную производительность, чтобы вручную развернуть свой String.format в StringBuilder.

Чтобы расширить / исправить первый ответ выше, это не перевод, с которым на самом деле справился бы String.format.
С помощью String.format вы будете печатать дату / время (или числовой формат и т. Д.), Где есть различия в локализации (l10n) (например, некоторые страны будут печатать 04Feb2009, а другие будут печатать Feb042009).
С переводом вы просто говорите о перемещении любых внешних строк (например, сообщений об ошибках и т. Д.) В набор свойств, чтобы вы могли использовать правильный пакет для правильного языка, используя ResourceBundle и MessageFormat.

Если посмотреть на все вышеизложенное, я бы сказал, что с точки зрения производительности, String.format против простой конкатенации сводится к тому, что вы предпочитаете. Если вы предпочитаете смотреть на вызовы .format по конкатенации, то, во всяком случае, идите с этим.
В конце концов, код читается намного больше, чем написано.

В вашем примере производительность probalby не слишком отличается, но есть и другие проблемы: fragmentация памяти. Даже операция конкатенации создает новую строку, даже если ее временная (для GC это требуется время, и это больше работает). String.format () является более читабельным и предполагает меньшую fragmentацию.

Кроме того, если вы используете определенный формат много, не забывайте, что вы можете напрямую использовать class 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)); 

Вторая форма, вероятно, более полезна, если вы делаете другие вещи одновременно.

Как правило, вы должны использовать String.Format, потому что он относительно быстрый и поддерживает глобализацию (предполагая, что вы на самом деле пытаетесь написать что-то, что читает пользователь). Это также облегчает глобализацию, если вы пытаетесь перевести одну строку в сравнении с 3 или более для каждого утверждения (особенно для языков, которые имеют совершенно разные грамматические структуры).

Теперь, если вы никогда не планируете переводить что-либо, то либо полагайтесь на Java, встроенный в преобразование + операторов в StringBuilder . Или используйте Java StringBuilder явно.

Еще одна перспектива с точки зрения регистрации только.

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

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

Вам действительно не нужно указывать / форматировать, если вы не хотите регистрироваться. Допустим, если я определю такой метод

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

В этом подходе cancat / formatter на самом деле не называется вообще, если его сообщение отладки и debugOn = false

Хотя здесь все равно будет лучше использовать StringBuilder вместо форматирования. Основная мотивация заключается в том, чтобы избежать этого.

В то же время мне не нравится добавлять блок «if» для каждого ведения журнала, поскольку

  • Это влияет на читаемость
  • Снижает охват моих модульных тестов – это путает, когда вы хотите убедиться, что каждая строка проверена.

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

Я только что модифицировал тест 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(.) от String.format(.) равен 23 на моей машине (16 с переключателем -server ).

Вот модифицированная версия записи 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 

}

Время после цикла 391 Время после цикла 4163 Время после цикла 227

Ответ на этот вопрос во многом зависит от того, как ваш специфический компилятор Java оптимизирует генерируемый байт-код. Строки неизменяемы и, теоретически, каждая операция «+» может создать новую. Но ваш компилятор почти наверняка оптимизирует промежуточные шаги в построении длинных строк. Вполне возможно, что обе строки кода выше генерируют точный байт-код.

Единственный реальный способ знать – проверить код итеративно в вашей текущей среде. Напишите приложение QD, которое объединяет строки в обоих направлениях итеративно и видит, как они тайминг друг против друга.

Подумайте об использовании "hello".concat( "world!" ) Для небольшого количества строк в конкатенации. Это может быть даже лучше для производительности, чем другие подходы.

Если у вас более трех строк, чем использование StringBuilder или просто String, в зависимости от используемого вами компилятора.

Давайте будем гением компьютера.