Должен ли я использовать String.format () Java, если производительность важна?
Мы должны постоянно создавать строки для вывода журнала и так далее. В версиях JDK мы узнали, когда использовать StringBuffer
(многие приложения, streamобезопасные) и StringBuilder
(многие присоединяются, не streamобезопасны).
Каков совет по использованию String.format()
? Является ли это эффективным, или мы вынуждены придерживаться конкатенации для однострочных линий, где важна производительность?
например, уродливый старый стиль,
- String.Format целое число, чтобы использовать разделитель тысяч без десятичных знаков или число 0 для малых целых чисел
- Использование String Format для отображения десятичной до 2-х мест или простого целого
- Предупреждение: «формат не строковый литерал и аргументы формата»
- Есть ли способ программно преобразовать строки форматирования VB6 в строки форматирования .NET?
- напечатать имя переменной в Matlab
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()
.
Я написал небольшой class для тестирования, который имеет лучшую производительность двух и +, опережает формат. в 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 ()), поэтому для используемой памяти другие подходы не будут затронуты. Я добавил больше конкатенаций, сделав строку «Blah» + i + «Blah» + i + «Blah» + i + «Blah».
Результат следующий (в среднем по 5 проходов каждый):
Время подхода (мс) Память выделена (длинная)
Оператор «+» 747 320 504
String.format 16484 373,312
StringBuilder 769 57 344
Мы можем видеть, что String ‘+’ и StringBuilder практически идентичны по времени, но StringBuilder намного эффективнее в использовании памяти. Это очень важно, когда у нас много вызовов журнала (или любых других операторов, содержащих строки) за короткий промежуток времени, поэтому сборщик мусора не сможет очистить множество экземпляров строк, вызванных оператором «+».
И примечание, BTW, не забудьте проверить уровень ведения журнала перед конструированием сообщения.
Выводы:
- Я буду продолжать использовать StringBuilder.
- У меня слишком много времени или слишком мало жизни.
Ваш старый уродливый стиль автоматически компилируется 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 ().
Все приведенные здесь контрольные показатели имеют некоторые недостатки , поэтому результаты не являются надежными.
Я был удивлен, что никто не использовал 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.
Таким образом, старый стиль (с использованием +) намного быстрее.
Java String.format работает так:
- он анализирует строку формата, взрываясь в список fragmentов формата
- он выполняет итерацию fragmentов формата, превращая их в StringBuilder, который в основном представляет собой массив, который по мере необходимости изменяет размер, путем копирования в новый массив. это необходимо, потому что мы еще не знаем, насколько велика будет выделение финальной строки
- StringBuilder.toString () копирует свой внутренний буфер в новую строку
если конечным пунктом назначения для этих данных является stream (например, создание веб-страницы или запись в файл), вы можете собрать fragmentы формата непосредственно в свой stream:
new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");
Я предполагаю, что оптимизатор оптимизирует обработку строк формата. Если это так, вы получите эквивалентную амортизированную производительность, чтобы вручную развернуть свой String.format в StringBuilder.
Чтобы расширить / исправить первый ответ выше, это не перевод, с которым на самом деле справился бы String.format.
С помощью String.format вы будете печатать дату / время (или числовой формат и т. Д.), Где есть различия в локализации (l10n) (например, некоторые страны будут печатать 04Feb2009, а другие будут печатать Feb042009).
С переводом вы просто говорите о перемещении любых внешних строк (например, сообщений об ошибках и т. Д.) В набор свойств, чтобы вы могли использовать правильный пакет для правильного языка, используя ResourceBundle и MessageFormat.
Если посмотреть на все вышеизложенное, я бы сказал, что с точки зрения производительности, String.format против простой конкатенации сводится к тому, что вы предпочитаете. Если вы предпочитаете смотреть на вызовы .format по конкатенации, то, во всяком случае, идите с этим.
В конце концов, код читается намного больше, чем написано.
В вашем примере производительность probalby не слишком отличается, но есть и другие проблемы: fragmentация памяти. Даже операция конкатенации создает новую строку, даже если ее временная (для GC это требуется время, и это больше работает). String.format () является более читабельным и предполагает меньшую fragmentацию.
Кроме того, если вы используете определенный формат много, не забывайте, что вы можете напрямую использовать class 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» для каждого ведения журнала, поскольку
- Это влияет на читаемость
- Снижает охват моих модульных тестов – это путает, когда вы хотите убедиться, что каждая строка проверена.
Поэтому я предпочитаю создавать служебный class журнала с такими методами, как указано выше, и использовать его везде, не беспокоясь об ударе по производительности и о любых других связанных с ним проблемах.
Я только что модифицировал тест 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(.)
от String.format(.)
равен 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
}
Время после цикла 391 Время после цикла 4163 Время после цикла 227
Ответ на этот вопрос во многом зависит от того, как ваш специфический компилятор Java оптимизирует генерируемый байт-код. Строки неизменяемы и, теоретически, каждая операция «+» может создать новую. Но ваш компилятор почти наверняка оптимизирует промежуточные шаги в построении длинных строк. Вполне возможно, что обе строки кода выше генерируют точный байт-код.
Единственный реальный способ знать – проверить код итеративно в вашей текущей среде. Напишите приложение QD, которое объединяет строки в обоих направлениях итеративно и видит, как они тайминг друг против друга.
Подумайте об использовании "hello".concat( "world!" )
Для небольшого количества строк в конкатенации. Это может быть даже лучше для производительности, чем другие подходы.
Если у вас более трех строк, чем использование StringBuilder или просто String, в зависимости от используемого вами компилятора.