Project Valhalla: эпичный квест Java за перфомансом

Project Valhalla, по моему скромному мнению, это самое важное и в то же время самое сложное нововведение в JVM, улучшающее производительность.

Бэкграунд: как модель памяти Java влияет на перфоманс

Рассмотрим, в чем разница между хранением массива примитивов int[] и массива объектов Integer[]:

Каждый объект в Java помимо полезной информации содержит еще и т.н. метаданные (или заголовок/хедер).

Для случая с примитивами int[] у нас есть только 1 объект - собственно массив. Его ячейки хранятся последовательно в памяти. Для случая с типами-обертками Integer[] у нас уже 5 объектов: массив и сами полезные данные отдельно. "Указатели" на объекты все еще хранятся последовательно в массиве, а вот полезные данные раскиданы где-то в памяти, и их хранение "рядом" в общем случае не гарантировано.

Это играет против перфоманса:

  1. Доступ к полезной информации для типов-оберток Integer требует больше операций с памятью

  2. Чтение данных из памяти происходит минимально возможными порциями - т.н. cacheline. Для большинства современных CPU размер этой порции равен 64 байтам. То есть если мы хотим прочитать значение типа int (4 байта) из определенной ячейки массива, то мы прочитаем еще несколько соседних ячеек. Для массива типов-оберток та же самая ситуация, но мы уже прочитаем из массива не саму полезную информацию, а "указатели", которые там хранятся. А это уже не так хорошо.

  3. Хедер с метаданными для каждого объекта занимает примерно 96-128 бит (после реализации Project Lilliput будет меньше). Поэтому для хранения целого числа (4 байта) в типе-обертке мы тратим 12-16 байт сверху. Помимо всего прочего, эти метаданные загружаются в кэши CPU, поэтому там остается меньше места для более полезных данных.

На момент релиза Java в 1995 году это было приемлемо, т.к. в те времена CPU и память работали со сравнимыми скоростями.

Однако сегодня, когда CPU значительно быстрее, чем память, такое расточительное отношение к последней становится бутылочным горлышком перфоманса.

Что за Project Valhalla?

Задуманный еще в 2014 (что дает представление, насколько сложна реализация), JEP-401 призван дать возможность создавать кастомные примитивные или "value" объекты. Эти объекты должны представлять собой плоскую структуру, подобно рассмотренному выше массиву int[], а не дерево "указателей". Разумеется, это не бесплатно, но это не тема данной статьи.

Рассмотрим, в чем разница по организации памяти, на примере класса Point

// Before
public class Point {
  private final int x;
  private final int y;
}

// After
public primitive class Point {
  private final int x;
  private final int y;
}
До и после применения модификатора primitive к классу
До и после применения модификатора primitive к классу

Значительно улучшается локальность памяти. Теперь, когда мы хотим просмотреть все точки, строка кэша, которая будет выбрана при доступе к p[0].x, будет содержать некоторые ячейки соседние ячейки p [0].y, p[1].x и т. д.

Также видно, что у нас теперь только 1 хедер с метаданными вместо четырех.

В Вальхаллу!

Мне очень хотелось протестировать эти примитивные классы в коллекциях, но, поскольку Project Valhalla в настоящее время находится в стадии раннего доступа, такая фича пока недоступна.

Проверим 2 сценария:

Окружение для теста:

GitHub: https://github.com/tomerr90/ProjectValhalla/tree/main

Benchmark               Mode  Cnt     Score     Error  Units
Valhalla.sort           avgt   25  7251.158 ± 302.564  us/op
Valhalla.sortPrimitive  avgt   25   747.968 ±  25.046  us/op
Valhalla.acc            avgt   25  3512.221 ± 160.482  us/op
Valhalla.accPrimitive   avgt   25   280.603 ±   4.226  us/op

Сортировка быстрее в 9.7 раз, а аккумулирование в 12.5!

Лично меня впечатляют не эти числа сами по себе, а тот факт, что такая старая платформа, как Java, все еще способна на фундаментальные изменения с заметным эффектом. Возможно, старую собаку таки можно научить новым фокусам.

@panzerfaust
18.01.2024 10:40 UTC
Первоисточник

Комментарии

@DenSigma
18.01.2024 09:38 UTC
+3

С сортировкой, чувствую, тут обман какой-то. По идее, сортировка массива указателей на объекты должно быть быстрее, чем сортировка плоских объектов, потому что вместо четырех (в данном случае) чисел нужно менять местами только два. Образно говоря, перевешиваются "номерки", вместо "чемоданов". Возможно, играет роль доступ к элементам для сравнения.

@Fyret
18.01.2024 09:52 UTC
+3

Прежде, чем перевешать "номерки", надо все-таки заглянуть в каждый "чемодан". А "чемоданы" в общем случае разбросаны по всему адресному пространству.

@igormich88
18.01.2024 11:33 UTC
0

Конкретно в этом примере размер "номерка" совпадает с размером "чемодана".

@ddruganov
18.01.2024 17:42 UTC
+3

Чувак, извини, случайно влепил тебе минус, хабр не даёт убрать :(

19.01.2024 01:19 UTC
+3

Я его откатил

@VADemon
19.01.2024 05:00 UTC
0

Процессор всё равно целую кеш-линию перезаписывать будет. Если хотите, прогоните его тесты под perf. JVM, конечно, внесет свой шум, но отдельно для объектов должно преобладать ожидание памяти в этом случае.

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

Третье, он отключил -XX:-TieredCompilation, поэтому какой именно код в итоге сгенерировала Hotspot и что вообще творилось - вопрос открытый.

@Lewigh
18.01.2024 11:31 UTC
0

У Project Valhalla есть один минус. Учитывая сколько его уже делают и сколько раз обещали что-нибудь наконец выкатить а воз и ныне там, есть ощущение что мы сами в Вальгалле окажемся раньше чем проект в релизе.

@vda19999
18.01.2024 15:45 UTC
0

А никаких конкретных планов, когда что-то из Вальхаллы мы увидем в релизе java?

@DenSigma
19.01.2024 05:41 UTC
0

Данный пример не очень релевантный, потому что можно исхитриться без Вальгаллы.

В случае, если в классе находятся примитивы одного типа, можно организовать класс с массивом двойной длины (в данном примере) и хранить в x в четных, y в нечетных ячейках массива. Организовать get и set соответствующим образом.

Буду признателен тому, кто подскажет, как несколько int из массива преобразовать в double. Иначе говоря, как в массиве int хранить значение double, без создания объектов, как это можно делать в C.

@ris58h
19.01.2024 07:47 UTC
0

хранить значение double, без создания объектов

Чем массив double не угодил?

@Falland
21.02.2024 11:45 UTC
0

Храните массив long и преобразуйте через Double.doubleToLongBits: http://docs.oracle.com/javase/7/docs/api/java/lang/Double.html#doubleToLongBits(double)

Если нужно прям int, то можно склеивать две ячейки перед преобразованием

@grisha9
19.01.2024 11:09 UTC
0

Помимо "локальности" данных, новые inline/primitive типы позволяют аллоцировать данные "на регистрах процессорах", не делая дополнительных обращений к опереативной памяти и экономить такты процессора. В целом и сейчас такие кейсы могут упрощаться на этапе escape анализа. За то "скидывать" новые типы данные в ОП или нет, отвечает JVM

@KvanTTT
19.01.2024 12:03 UTC
+3

На дворе 2024, а в Java и JVM до сих пор не завезли value типы :(

@iliks84
22.01.2024 05:56 UTC
+1

10 лет назад, прочитав про этот проект, я подумал: о, круто, наконец-то завезут фичу из первого дотнета, начала 2000х)