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

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

Какой совет по использованию 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().

Ответ 1

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

Runtime comparison

Ответ 2

Я взял 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" + я + "Blah" + я + "Blah" + я + "Blah".

Результат следующий (в среднем по 5 прогонов каждый):
Подход       Время (мс)   Память выделена (длинная)
"+" -                       320,504
String.Format   16484       373,312
StringBuilder   769           57344

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

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

Выводы:

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

Ответ 3

Ваш старый уродливый стиль автоматически компилируется 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().

Ответ 4

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

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

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

Ответ 7

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

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

См. этот связанный вопрос Является ли String.Format таким же эффективным, как StringBuilder?.

Кроме того, что-то еще вы должны знать: будьте осторожны с использованием 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

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

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

Ответ 9

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

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

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

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

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

Ответ 10

Я только что модифицировал тест 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).

Ответ 11

Здесь представлена ​​измененная версия записи 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

Ответ 12

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

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

Ответ 13

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

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