Должен ли я использовать 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напрямую.

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


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, а не на конкатенацию, то обязательно используйте это.
В конце концов, код читается намного больше, чем написано.


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

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

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


Не могли бы вы использовать существующую библиотеку, такую ​​как 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переключателем).


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


0

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

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


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

Я использовал «байт-код», где я должен был сказать «двоичный код». Когда все сводится к JMP и MOVS, это может быть точно такой же код.
— Да, это Джейк.

0

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.