Должен ли я использовать 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() в частности.

13 ответов

Решение

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

Результат выглядит следующим образом (в среднем по 5 прогонов каждый):
Время приближения (мс) Память распределена (долго)
оператор "+" 747           320 504
Строка.формат 16484 373,312
StringBuilder 769 57,344

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

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

Выводы:

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

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

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

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

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

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

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

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

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

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

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

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

В вашем примере производительность 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));

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

Как правило, вы должны использовать 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" для каждого оператора регистрации, так как

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

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

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

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

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

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

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

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

Другие вопросы по тегам