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

StackOverflow https://stackoverflow.com/questions/513600

Вопрос

Нам приходится постоянно создавать строки для вывода журнала и так далее.Из версий 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() в частности.

Это было полезно?

Решение

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

Runtime comparison

Другие советы

Я взял хафез код и добавил проверка памяти:

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

Я запускаю это отдельно для каждого подхода, оператора '+', String.format и StringBuilder (вызывающего toString()), поэтому другие подходы не повлияют на используемую память.Я добавил больше конкатенаций, сделав строку как "Бла" + i + "Бла" + i + "Бла" + i + "Бла".

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

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

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

Выводы:

  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

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

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

Ваш старый уродливый стиль автоматически компилируется 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 намного более тяжелый, поскольку он создает новый форматировщик, анализирует вашу входную строку формата, создает 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, а другие - Feb042009).
С переводом вы просто говорите о перемещении любых экстернализуемых строк (например, сообщений об ошибках и чего-то еще) в пакет свойств, чтобы вы могли использовать правильный пакет для правильного языка, используя ResourceBundle и MessageFormat .

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

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

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

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

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

На самом деле вам не нужно объединять / форматировать, если вы не хотите войти в систему.Допустим, если я определю такой метод, как этот

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 вместо formatter.Главная мотивация состоит в том, чтобы избежать всего этого.

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

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

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

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

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

}

}

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top