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 к классу

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

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

В Вальхаллу!

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

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

  • Сортировка миллиона Point по Х и Y

  • Подсчет суммы миллиона Point, используя иммутабельный аккумулятор

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

  • Java 22-valhalla

  • 64 GB RAM

  • Intel(R) Core(TM) i7–11850H

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, все еще способна на фундаментальные изменения с заметным эффектом. Возможно, старую собаку таки можно научить новым фокусам.