Подсказки производительности строки
1. Введение
В этом учебнике мы сосредоточимся на аспекте производительности api Java String .
Мы будем копаться в Струнные операции по созданию, преобразованию и модификации для анализа доступных вариантов и сравнения их эффективности.
Предложения, которые мы собираемся сделать, не обязательно подходят для каждого приложения. Но, конечно, мы собираемся показать, как выиграть на производительность, когда время работы приложения имеет решающее значение.
2. Строительство новой струны
Как вы знаете, в Java строки непреложны. Поэтому каждый раз, когда мы строим или конкации Струнные объект, Java создает новую Строка – это может быть особенно дорогостоящим, если сделать в цикле.
2.1. Использование конструктора
В большинстве случаев мы должны избегать создания Струны используя конструктора, если мы не знаем, что мы делаем .
Давайте создадим newString объект внутри петли во-первых, используя новая строка () конструктор, то = оператор.
Чтобы написать наш тест, мы будем использовать инструмент JMH (Java Microbenchmark Harness).
Наша конфигурация:
@BenchmarkMode(Mode.SingleShotTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Measurement(batchSize = 10000, iterations = 10) @Warmup(batchSize = 10000, iterations = 10) public class StringPerformance { }
Вот, мы используем SingeShotTime режим, который запускает метод только один раз. Как мы хотим измерить производительность Струнные операций внутри петли, есть @Measurement аннотация доступна для этого.
Важно знать, что бенчмаркинг циклов непосредственно в наших тестах может исказить результаты из-за различных оптимизаций, применяемых JVM .
Таким образом, мы вычисляем только одну операцию и позволяем JMH заботиться о цикле. Короче говоря, JMH выполняет итерации с помощью пакетСейные параметр.
Теперь добавим первый микро-бенчмарк:
@Benchmark public String benchmarkStringConstructor() { return new String("baeldung"); } @Benchmark public String benchmarkStringLiteral() { return "baeldung"; }
В первом тесте в каждой итерации создается новый объект. Во втором тесте объект создается только один раз. Для остальных итераций один и тот же объект возвращается из Струна постоянный бассейн.
Давайте забудем тесты с подсчетом циклических итераций 000 000 и увидеть результаты:
Benchmark Mode Cnt Score Error Units benchmarkStringConstructor ss 10 16.089 ± 3.355 ms/op benchmarkStringLiteral ss 10 9.523 ± 3.331 ms/op
Из Оценка значения, мы можем ясно видеть, что разница является значительным.
2.2. Оператор
Давайте посмотрим на динамические Струнные пример конкатенации:
@State(Scope.Thread) public static class StringPerformanceHints { String result = ""; String baeldung = "baeldung"; } @Benchmark public String benchmarkStringDynamicConcat() { return result + baeldung; }
В наших результатах мы хотим видеть среднее время выполнения. Формат номера вывода устанавливается на миллисекунды:
Benchmark 1000 10,000 benchmarkStringDynamicConcat 47.331 4370.411
Теперь давайте проанализируем результаты. Как видим, добавляя 1000 предметы для state.result занимает 47.331 Миллисекунд. Следовательно, увеличивая количество итераций в 10 раз, время работы увеличивается до 4370,441 Миллисекунд.
Таким образом, время исполнения растет четырехкратно. Таким образом, сложность динамической конкатенации в цикле n итераций O (n-2) .
2.3. String.concat()
Еще один способ конкатенировать Струны с помощью concat () метод:
@Benchmark public String benchmarkStringConcat() { return result.concat(baeldung); }
Единица времени вывода составляет миллисекунду, количество итераций — 100 000. Таблица результатов выглядит так:
Benchmark Mode Cnt Score Error Units benchmarkStringConcat ss 10 3403.146 ± 852.520 ms/op
2.4. Струнный формат ()
Еще один способ создания строк с помощью String.format () метод. Под капотом, он использует регулярные выражения, чтобы разобрать входные данные.
Давайте напишем тестовый случай JMH:
String formatString = "hello %s, nice to meet you"; @Benchmark public String benchmarkStringFormat_s() { return String.format(formatString, baeldung); }
После этого мы запускаем его и видим результаты:
Number of Iterations 10,000 100,000 1,000,000 benchmarkStringFormat_s 17.181 140.456 1636.279 ms/op
Хотя код с String.format () выглядит более чистым и читаемым, мы не победим здесь с зрения производительности.
2.5. StringBuilder и StringBuffer
У нас уже есть записи объясняя СтрингБуффер и Стрингбилдер . Так что здесь мы покажем только дополнительную информацию об их исполнении. Стрингбилдер использует переиззируемый массив и индекс, указывающий на положение последней ячейки, используемой в массиве. Когда массив полон, он расширяется в два раза от его размера и копирует все символы в новый массив.
Принимая во внимание, что размер не происходит очень часто, мы можем рассмотреть каждый приложение () эксплуатации как O(1) постоянное время . Учитывая это, весь процесс уже О(н) сложность.
После изменения и запуска динамического теста на конкатенацию для СтрингБуффер и StringBuilder, мы получаем:
Benchmark Mode Cnt Score Error Units benchmarkStringBuffer ss 10 1.409 ± 1.665 ms/op benchmarkStringBuilder ss 10 1.200 ± 0.648 ms/op
Хотя разница в счете не так много, мы можем заметить, что Стрингбилдер работает быстрее .
К счастью, в простых случаях, мы не нуждаемся в Стрингбилдер поставить одну Струнные с другим. Иногда статическая конатенация с q может фактически заменить Стрингбилдер . Под капотом последние компиляторы Java вызовут StringBuilder.append() для конкатенации строк .
Это означает, что победа в производительности значительно.
3. Коммунальные операции
3.1. StringUtils.replace() против String.replace()
Интересно знать, что Версия Apache Commons для замены Струнные делает намного лучше, чем собственные заменить () метод . Ответ на это различие лежит под их осуществлением. String.replace () использует шаблон regex, чтобы соответствовать струна.
В отличие от этого, StringUtils.replace () широко использует indexOf () , что быстрее.
Теперь пришло время для контрольных тестов:
@Benchmark public String benchmarkStringReplace() { return longString.replace("average", " average !!!"); } @Benchmark public String benchmarkStringUtilsReplace() { return StringUtils.replace(longString, "average", " average !!!"); }
Установка пакетСейные до 100 000, мы представляем результаты:
Benchmark Mode Cnt Score Error Units benchmarkStringReplace ss 10 6.233 ± 2.922 ms/op benchmarkStringUtilsReplace ss 10 5.355 ± 2.497 ms/op
Хотя разница между числами не слишком велика, StringUtils.replace () имеет лучший результат. Конечно, цифры и разрыв между ними могут варьироваться в зависимости от параметров, таких как количество итераций, длина строки и даже версия JDK.
С последними версиями JDK 9 (наши тесты работают на JDK 10) обе реализации имеют довольно равные результаты. Теперь, давайте понизить версию JDK до 8 и тесты снова:
Benchmark Mode Cnt Score Error Units benchmarkStringReplace ss 10 48.061 ± 17.157 ms/op benchmarkStringUtilsReplace ss 10 14.478 ± 5.752 ms/op
Разница в производительности огромна сейчас и подтверждает теорию, которую мы обсуждали в начале.
3.2. раскол ()
Перед запуском полезно проверить методы разделения строк, доступные на Java.
Когда возникает необходимость разделить строку с делимитером, первая функция, которая приходит на ум, как правило, String.split (regex) . Тем не менее, это приносит некоторые серьезные проблемы с производительностью, так как он принимает аргумент regex. Кроме того, мы можем использовать СтрингТокенизер класса, чтобы разбить строку на токены.
Другим вариантом является Сплиттер API. Наконец, старые добрые indexOf () также доступен для повышения производительности нашего приложения, если нам не нужна функциональность регулярных выражений.
Теперь пришло время написать тесты для String.split() выбор:
String emptyString = " "; @Benchmark public String [] benchmarkStringSplit() { return longString.split(emptyString); }
Шаблон.сплит () :
@Benchmark public String [] benchmarkStringSplitPattern() { return spacePattern.split(longString, 0); }
СтрингТокенизер :
List stringTokenizer = new ArrayList<>(); @Benchmark public List benchmarkStringTokenizer() { StringTokenizer st = new StringTokenizer(longString); while (st.hasMoreTokens()) { stringTokenizer.add(st.nextToken()); } return stringTokenizer; }
String.indexOf() :
List stringSplit = new ArrayList<>(); @Benchmark public List benchmarkStringIndexOf() { int pos = 0, end; while ((end = longString.indexOf(' ', pos)) >= 0) { stringSplit.add(longString.substring(pos, end)); pos = end + 1; } return stringSplit; }
Гуава Сплиттер :
@Benchmark public ListbenchmarkGuavaSplitter() { return Splitter.on(" ").trimResults() .omitEmptyStrings() .splitToList(longString); }
Наконец, мы запускаем и сравниваем результаты для batchSize,000 :
Benchmark Mode Cnt Score Error Units benchmarkGuavaSplitter ss 10 4.008 ± 1.836 ms/op benchmarkStringIndexOf ss 10 1.144 ± 0.322 ms/op benchmarkStringSplit ss 10 1.983 ± 1.075 ms/op benchmarkStringSplitPattern ss 10 14.891 ± 5.678 ms/op benchmarkStringTokenizer ss 10 2.277 ± 0.448 ms/op
Как мы видим, худшие показатели бенчмаркСтрингСплитПаттерн метод, где мы используем Шаблон класс. В результате мы можем узнать, что с помощью класса regex с раскол () метод может привести к потере производительности в несколько раз.
Аналогичным образом, мы замечаем, что самые быстрые результаты являются приведение примеров с использованием indexOf() и сплит() .
3.3. Преобразование в строку
В этом разделе мы измерим показатели конверсии строки в течение времени выполнения. Чтобы быть более конкретным, мы рассмотрим Integer.toString () метод конкатенации:
int sampleNumber = 100; @Benchmark public String benchmarkIntegerToString() { return Integer.toString(sampleNumber); }
String.valueOf() :
@Benchmark public String benchmarkStringValueOf() { return String.valueOf(sampleNumber); }
«некоторая ценная стоимость» и «» :
@Benchmark public String benchmarkStringConvertPlus() { return sampleNumber + ""; }
String.format () :
String formatDigit = "%d"; @Benchmark public String benchmarkStringFormat_d() { return String.format(formatDigit, sampleNumber); }
После запуска тестов мы увидим выход для batchSize,000 :
Benchmark Mode Cnt Score Error Units benchmarkIntegerToString ss 10 0.953 ± 0.707 ms/op benchmarkStringConvertPlus ss 10 1.464 ± 1.670 ms/op benchmarkStringFormat_d ss 10 15.656 ± 8.896 ms/op benchmarkStringValueOf ss 10 2.847 ± 11.153 ms/op
Проанализировав результаты, мы видим, что тест на Integer.toString () имеет лучший результат 0,953 миллисекунды . В отличие от этого, преобразование, которое включает String.format (“%d”) имеет худшие показатели.
Это логично, потому что разбор формата Струнные это дорогостоящая операция.
3.4. Сравнение струн
Давайте оценим различные способы сравнения Строки. Количество итераций 100 000 .
Вот наши контрольные тесты для String.equals () операция:
@Benchmark public boolean benchmarkStringEquals() { return longString.equals(baeldung); }
String.equalsIgnoreCase () :
@Benchmark public boolean benchmarkStringEqualsIgnoreCase() { return longString.equalsIgnoreCase(baeldung); }
String.matches () :
@Benchmark public boolean benchmarkStringMatches() { return longString.matches(baeldung); }
String.compareTo() :
@Benchmark public int benchmarkStringCompareTo() { return longString.compareTo(baeldung); }
После этого мы запускаем тесты и отображаем результаты:
Benchmark Mode Cnt Score Error Units benchmarkStringCompareTo ss 10 2.561 ± 0.899 ms/op benchmarkStringEquals ss 10 1.712 ± 0.839 ms/op benchmarkStringEqualsIgnoreCase ss 10 2.081 ± 1.221 ms/op benchmarkStringMatches ss 10 118.364 ± 43.203 ms/op
Как всегда, цифры говорят сами за себя. спички () занимает больше времени, поскольку он использует regex для сравнения равенства.
В отличие от этого, равны () и равняетсяИнорАс () являются лучшим выбором .
3.5. String.matches() против предварительной схемы
Теперь, давайте отдельно посмотрим на String.matches () и Matcher.matches () Шаблоны. Первый принимает regexp в качестве аргумента и компилирует его перед выполнением.
Поэтому каждый раз, когда мы String.matches () , он компилирует рисунок:
@Benchmark public boolean benchmarkStringMatches() { return longString.matches(baeldung); }
Второй метод повторно использует Шаблон объект:
Pattern longPattern = Pattern.compile(longString); @Benchmark public boolean benchmarkPrecompiledMatches() { return longPattern.matcher(baeldung).matches(); }
А теперь результаты:
Benchmark Mode Cnt Score Error Units benchmarkPrecompiledMatches ss 10 29.594 ± 12.784 ms/op benchmarkStringMatches ss 10 106.821 ± 46.963 ms/op
Как мы видим, сопоставление с предварительно regexp работает примерно в три раза быстрее.
3.6. Проверка длины
Наконец, давайте сравним String.isEmpty () метод:
@Benchmark public boolean benchmarkStringIsEmpty() { return longString.isEmpty(); }
и String.length () метод:
@Benchmark public boolean benchmarkStringLengthZero() { return emptyString.length() == 0; }
Во-первых, мы звоним им по longString String. пакетСейные это 10 000 :
Benchmark Mode Cnt Score Error Units benchmarkStringIsEmpty ss 10 0.295 ± 0.277 ms/op benchmarkStringLengthZero ss 10 0.472 ± 0.840 ms/op
После, давайте установить longString пустой строки и запустить тесты снова:
Benchmark Mode Cnt Score Error Units benchmarkStringIsEmpty ss 10 0.245 ± 0.362 ms/op benchmarkStringLengthZero ss 10 0.351 ± 0.473 ms/op
Как мы замечаем, бенчмаркСтрингЛенгтеро () и бенчмаркStringIsEmpty () методы в обоих случаях имеют примерно одинаковую оценку. Тем не менее, isEmpty () работает быстрее, чем проверка, равна ли длина строки нулю .
4. Дедупликация струн
Начиная с JDK 8, функция ддупликации строк доступна для устранения потребления памяти. Проще говоря, этот инструмент ищет строки с тем же или дублировать содержимое для хранения одной копии каждого отдельного значения строки в пул строк .
В настоящее время существует два способа справиться с Струнные Дубликаты:
- с помощью String.intern () вручную
- включение дедупляции строки
Давайте более подробно рассмотрим каждый вариант.
4.1. Стринг.интерн()
Прежде чем прыгать вперед, будет полезно прочитать о ручном интернировании в нашем записи . С String.intern () мы можем вручную установить ссылку на Струнные объект внутри глобального Струнные бассейн .
Затем JVM может использовать возврат ссылки, когда это необходимо. С точки зрения производительности, наше приложение может извлечь огромную пользу, повторно излив ссылки строки из постоянного пула.
Важно знать, что СПМ Струнные бассейн не является локальным для потока. Каждая Струнные что мы добавляем в пул, доступен для других потоков, а также .
Однако есть и серьезные недостатки:
- для правильного обслуживания нашего приложения нам, возможно, потребуется установить -XX:StringTableSize Параметр JVM для увеличения размера пула. JVM нуждается в перезагрузке, чтобы расширить размер пула
- вызов String.intern () вручную является трудоемким . Он растет в линейном алгоритме времени с О(н) сложность
- кроме того, частые звонки на длинные Струнные объекты могут вызвать проблемы с памятью
Чтобы иметь некоторые проверенные номера, давайте забудем тест:
@Benchmark public String benchmarkStringIntern() { return baeldung.intern(); }
Кроме того, результаты вывода в миллисекундах:
Benchmark 1000 10,000 100,000 1,000,000 benchmarkStringIntern 0.433 2.243 19.996 204.373
Заготовки столбцев здесь представляют собой другой итерации счета из 1000 1 000 000 . Для каждого номера итерации у нас есть оценка производительности теста. Как мы замечаем, оценка резко возрастает в дополнение к числу итераций.
4.2. Включить дедупликацию автоматически
Прежде всего, этот параметр является частью сборщика мусора G1. По умолчанию эта функция отключена. Таким образом, мы должны включить его со следующей командой:
-XX:+UseG1GC -XX:+UseStringDeduplication
Важно отметить, что включение этой опции не гарантирует, что Струнные дедупликация произойдет . Кроме того, он не обработать молодых Строки. Для того, чтобы управлять минимальным возрастом обработки Струны, Вариант JVM доступен. Вот, 3 параметр по умолчанию.
5. Резюме
В этом учебнике мы пытаемся дать некоторые подсказки, чтобы использовать строки более эффективно в нашей повседневной жизни кодирования.
В результате мы можем выделить некоторые предложения, чтобы повысить производительность наших приложений :
- при конкатенатации струн, Стрингбилдер является наиболее удобным вариантом что приходит на ум. Тем не менее, с небольшими строками, + операция имеет почти такую же производительность. Под капотом компилятор Java может использовать Стрингбилдер класс для уменьшения количества объектов строки
- для преобразования значения в строку, (некоторый тип).toString () ( Integer.toString () например) работает быстрее, чем String.valueOf() . Поскольку эта разница не значительна, мы можем свободно использовать String.valueOf() не иметь зависимости от типа входного значения
- когда дело доходит до сравнения строки, ничто не сравнится с String.equals () до сих пор
- Струнные дедупликация повышает производительность в больших многотемных приложениях. Но чрезмерное String.intern () может привести к серьезным утечкам памяти, замедляя приложение
- для разделения строк мы должны использовать indexOf () чтобы выиграть в производительности . Однако в некоторых некритических случаях String.split() функция может быть хорошо подходят
- Использование Шаблон.матч () строка значительно повышает производительность
- String.isEmpty () быстрее, чем строка длина ()
Кроме того, имейте в виду, что цифры, которые мы представляем здесь, являются лишь результатами JMH – поэтому вы всегда должны тестировать в области собственной системы и времени выполнения, чтобы определить влияние такого рода оптимизации.
Наконец, как всегда, код, используемый во время обсуждения, можно найти более на GitHub .