Перейти к основному содержимому

Компактные строки в Java 9

· 5 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

Строки в Java внутренне представлены char[] , содержащим символы String . И каждый char состоит из 2 байтов, потому что Java внутри использует UTF-16.

Например, если строка содержит слово на английском языке, все первые 8 битов будут равны 0 для каждого char , поскольку символ ASCII может быть представлен одним байтом.

Для представления многих символов требуется 16 бит, но по статистике для большинства требуется всего 8 бит — представление символов LATIN-1. Таким образом, есть возможность улучшить потребление памяти и производительность.

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

В этой статье мы обсудим опцию Compressed String, представленную в JDK6, и новую компактную строку, недавно представленную в JDK9. Оба они были разработаны для оптимизации потребления памяти строками в JMV.

2. Сжатая строка — Java 6

В обновлении JDK 6 21 Performance Release появилась новая опция виртуальной машины:

-XX:+UseCompressedStrings

Когда эта опция включена, строки сохраняются как byte[] вместо char[] — таким образом экономится много памяти. Однако в конечном итоге этот параметр был удален в JDK 7, главным образом потому, что он имел некоторые непредвиденные последствия для производительности.

3. Компактная строка — Java 9

Java 9 вернула концепцию компактных строк .

Это означает, что всякий раз, когда мы создаем строку , если все символы строки могут быть представлены с использованием байтового представления LATIN-1, внутри будет использоваться массив байтов , так что один байт дается для одного символа.

В других случаях, если для представления какого-либо символа требуется более 8 бит, все символы хранятся с использованием двух байтов для каждого — представление UTF-16.

Так что в основном, когда это возможно, он будет использовать один байт для каждого символа.

Теперь вопрос: как будут работать все операции со строками ? Как он будет различать представления LATIN-1 и UTF-16?

Ну, чтобы решить эту проблему, во внутреннюю реализацию String внесено еще одно изменение . У нас есть окончательный кодировщик поля , который сохраняет эту информацию.

3.1. Строковая реализация в Java 9

До сих пор строка хранилась как char[] :

private final char[] value;

Отныне это будет byte[]:

private final byte[] value;

Кодировщик переменных :

private final byte coder;

Где может быть кодер :

static final byte LATIN1 = 0;
static final byte UTF16 = 1;

Большинство операций со строками теперь проверяют кодировщик и направляют его в конкретную реализацию:

public int indexOf(int ch, int fromIndex) {
return isLatin1()
? StringLatin1.indexOf(value, ch, fromIndex)
: StringUTF16.indexOf(value, ch, fromIndex);
}

private boolean isLatin1() {
return COMPACT_STRINGS && coder == LATIN1;
}

Когда вся информация, необходимая JVM, готова и доступна, опция CompactString VM включена по умолчанию. Чтобы отключить его, мы можем использовать:

+XX:-CompactStrings

3.2. Как работает кодер

В реализации класса Java 9 String длина рассчитывается как:

public int length() {
return value.length >> coder;
}

Если строка содержит только LATIN-1, значение кодера будет равно 0, поэтому длина строки будет такой же, как длина массива байтов.

В других случаях, если String имеет представление UTF-16, значение coder будет равно 1, и, следовательно, длина будет вдвое меньше размера фактического массива байтов.

Обратите внимание, что все изменения, сделанные для Compact String, относятся к внутренней реализации класса String и полностью прозрачны для разработчиков, использующих String .

4. Компактные строки против сжатых строк

В случае со сжатыми строками JDK 6 основная проблема заключалась в том, что конструктор String принимал в качестве аргумента только char[] . В дополнение к этому, многие операции со строками зависели от представления char[] , а не от массива байтов. Из-за этого приходилось много распаковывать, что сказывалось на производительности.

Принимая во внимание, что в случае компактной строки сохранение дополнительного поля «кодер» также может увеличить накладные расходы. Чтобы уменьшить стоимость кодера и распаковки byte в char (в случае представления UTF-16), некоторые методы были встроены , а код ASM, сгенерированный JIT-компилятором, также был улучшен.

Это изменение привело к некоторым нелогичным результатам. LATIN-1 indexOf(String) вызывает встроенный метод, а indexOf(char) – нет. В случае UTF-16 оба эти метода вызывают встроенный метод. Эта проблема затрагивает только строку LATIN-1 и будет исправлена в будущих выпусках.

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

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

4.1. Разница в производительности

Давайте посмотрим на очень простой пример разницы в производительности между включением и отключением компактных строк:

long startTime = System.currentTimeMillis();

List strings = IntStream.rangeClosed(1, 10_000_000)
.mapToObj(Integer::toString)
.collect(toList());

long totalTime = System.currentTimeMillis() - startTime;
System.out.println(
"Generated " + strings.size() + " strings in " + totalTime + " ms.");

startTime = System.currentTimeMillis();

String appended = (String) strings.stream()
.limit(100_000)
.reduce("", (l, r) -> l.toString() + r.toString());

totalTime = System.currentTimeMillis() - startTime;
System.out.println("Created string of length " + appended.length()
+ " in " + totalTime + " ms.");

Здесь мы создаем 10 миллионов String , а затем добавляем их наивным образом. Когда мы запускаем этот код (компактные строки включены по умолчанию), мы получаем вывод:

Generated 10000000 strings in 854 ms.
Created string of length 488895 in 5130 ms.

Точно так же, если мы запустим его, отключив компактные строки с помощью опции: -XX:-CompactStrings , вывод будет следующим:

Generated 10000000 strings in 936 ms.
Created string of length 488895 in 9727 ms.

Очевидно, что это поверхностный тест, и он не может быть очень репрезентативным — это всего лишь снимок того, что новая опция может сделать для повышения производительности в этом конкретном сценарии.

5. Вывод

В этом руководстве мы видели попытки оптимизировать производительность и потребление памяти на JVM — путем хранения String с эффективным использованием памяти.

Как всегда, весь код доступен на Github .