Должен ли я использовать 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 намного более эффективен в использовании памяти. Это очень важно, когда у нас есть много вызовов журнала (или любых других операторов, включающих строки) за достаточно короткий промежуток времени, поэтому сборщик мусора не сможет очистить множество строковых экземпляров, полученных в результате оператора '+'.
И заметьте, кстати, не забудьте проверить уровень ведения журнала перед созданием сообщения.
Выводы:
- Я буду продолжать использовать StringBuilder.
- У меня слишком много времени или слишком мало жизни.
Все представленные здесь тесты имеют некоторые недостатки, поэтому результаты не являются надежными.
Я был удивлен, что никто не использовал 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 работает так:
- он анализирует строку формата, разбиваясь на список фрагментов формата
- он повторяет фрагменты формата, рендеринг в StringBuilder, который в основном является массивом, который изменяет размеры по мере необходимости, копируя в новый массив. это необходимо, потому что мы еще не знаем, насколько велика для выделения финальная строка
- 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, которое объединяет строки в обе стороны итеративно, и посмотрите, как они выдерживают время друг против друга.