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

216

Мы должны строить строки все время для вывода журнала и так далее. В версиях 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);

Примечание: мой конкретный вариант использования - это сотни строк журнала «с одной строкой» в моем коде. Они не содержат петли, поэтому 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
Мэтт Пасселл

Ответы:

124

Я написал небольшой класс для тестирования, который имеет лучшую производительность, чем два, и + опережает формат. в 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
источник
12
В этом тесте есть один недостаток, заключающийся в том, что он не совсем хорошо отображает все форматирование строк. Часто есть логика в том, что включать, и логика для форматирования определенных значений в строки. Любой реальный тест должен смотреть на реальные сценарии.
Орион Адриан
9
Был еще один вопрос на SO о + verses StringBuffer, в последних версиях Java + был заменен на StringBuffer, когда это было возможно, чтобы производительность не отличалась
hhafez
25
Это очень похоже на микробенчмарк, который будет оптимизирован очень бесполезным способом.
Дэвид Х. Клементс
20
Еще один плохо реализованный микро-тест. Как оба метода масштабируются на несколько порядков. Как насчет использования, 100, 1000, 10000, 1000000, операций. Если вы запускаете только один тест, на один порядок, в приложении, которое не работает на изолированном ядре; невозможно сказать, сколько различий можно списать как «побочные эффекты» из-за переключения контекста, фоновых процессов и т. д.
Эван Плейс,
8
Более того, так как вы никогда не выходите из основного JIT, вы не можете его запустить.
Jan Zyka
242

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

Результат выглядит следующим образом (в среднем по 5 прогонов каждый):
Время подхода (мс)
Оператор выделения памяти (длинный) Оператор «+» 747 320
504 String.format 16484 373 312 StringBuilder
769 57 344

Мы видим, что String '+' и StringBuilder практически идентичны по времени, но StringBuilder намного более эффективен в использовании памяти. Это очень важно, когда у нас много вызовов журнала (или любых других операторов, включающих строки) за достаточно короткий промежуток времени, поэтому сборщик мусора не сможет очистить множество строковых экземпляров, полученных в результате оператора '+'.

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

Выводы:

  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

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

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

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

Рафаель
источник
С точки зрения читабельности код, который вы разместили, гораздо более ... громоздок, чем String.format ("Что вы получите, если умножите% d на% d?", VarSix, varNine);
dusktreader
12
Нет разницы между +и StringBuilderдействительно. К сожалению, есть много дезинформации в других ответах в этой теме. Я почти соблазн изменить вопрос на how should I not be measuring performance.
CurtainDog
12

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

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

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

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(недостаток стандартной библиотеки Java, IMO, тем более что вы не можете кэшировать проанализированную строку формата).
Энди МакКинлей,
8

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

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

dw.mackie
источник
1
Я бы сказал, что с точки зрения производительности, String.format и простой конкатенации сводятся к тому, что вы предпочитаете, я думаю, что это неправильно. С точки зрения производительности сцепление намного лучше. Для более подробной информации, пожалуйста, посмотрите на мой ответ.
Адам Stelmaszczyk
6

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

Орион Адриан
источник
3

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

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

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

Вам на самом деле не нужно выполнять concat / format, если вы не хотите войти. Скажем, если я определю такой метод

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» для каждого оператора регистрации, так как

  • Это влияет на читабельность
  • Уменьшает охват моих юнит-тестов - это сбивает с толку, когда вы хотите убедиться, что каждая строка тестируется.

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

software.wikipedia
источник
Не могли бы вы использовать существующую библиотеку, такую ​​как slf4j-api, которая предназначена для решения этого варианта использования с их параметризованной функцией ведения журнала? slf4j.org/faq.html#logging_performance
Аммиан
2

Я только что изменил тест Хафеза, чтобы включить 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переключателем).

the.duckman
источник
1
Ваш тест недействителен, потому что он не учитывает время, затраченное только на цикл. Вы должны включить это и вычесть это из всех других результатов, как минимум (да, это может быть значительный процент).
Клет
Я сделал это, цикл for занимает 0 мс. Но даже если бы это заняло время, это только увеличило бы фактор.
the.duckman
3
Тест StringBuilder недопустим, потому что он не вызывает toString () в конце, чтобы фактически дать вам строку, которую вы можете использовать. Я добавил это, и в результате 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

ANON
источник
0

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

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

Да, это Джейк.
источник
1
Байт-код для второго примера наверняка вызывает String.format, но я был бы в ужасе, если бы простая конкатенация сделала. Зачем компилятору использовать строку формата, которую затем нужно будет проанализировать?
Джон Скит
Я использовал «байт-код», где я должен был сказать «двоичный код». Когда все сводится к JMP и MOVS, это может быть точно такой же код.
Да, это Джейк.
0

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

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

Sasa
источник