Рубрики
Без рубрики

Подсказки производительности строки

Узнайте об аспектах производительности API Java String

Автор оригинала: baeldung.

Подсказки производительности строки

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 List benchmarkGuavaSplitter() {
    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 .