Vector API в Java: краткий обзор и тестирование

Некоторое время назад попалась на глаза статья про Vector API в Java.


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


Результаты немного неоднозначные.


Краткое описание

Итак, Vector API в Java сейчас — это модуль, который предоставляет возможность выполнять векторные вычисления, ускоряемые аппаратно. Является частью Project Panama.


В данный момент (Java 18) модуль находится в стадии третьего инкубатора.


Ключевая особенность — использование SIMD (single instruction, multiple data) — т.е. выполнение математической операции над несколькими скалярными значениями ( == вектором ) одновременно, что в теории может дать ускорение вычислений.


Известная всем картинка про SIMD


Вот как это выглядит в базовом случае:


float[] a = new float[] {0.1F, 0.2F, 0.3F, 0.4F};
float[] b = new float[] {0.5F, 0.6F, 0.7F, 0.8F};

FloatVector va = FloatVector.fromArray(FloatVector.SPECIES_128, a, 0);
FloatVector vb = FloatVector.fromArray(FloatVector.SPECIES_128, b, 0);

FloatVector result = va.add(vb).div(4F).pow(2F).neg();

Формируем векторы типа FloatVector из массива 4 float.


Константа FloatVector.SPECIES_128 определяет тип вектора:



См. подробнее здесь.


Таким образом, данный вектор хранит 4 значения типа float, т.е. 4 линии (lanes).


Теперь завернем это в цикл для обработки массивов большей длины:


final VectorSpecies<Float> SPECIES_FLOAT = FloatVector.SPECIES_128;
final int length = 10_000;
final int upperBound = SPECIES_FLOAT.loopBound(length);

float[] a = getArrayOfFloats(length);
float[] b = getArrayOfFloats(length);
float[] result = new float[length];

// вариант первый
for (int i = 0; i < upperBound; i += SPECIES_FLOAT.length()) {
    VectorMask<Float> mask = SPECIES_FLOAT.indexInRange(i, upperBound);
    FloatVector va = FloatVector.fromArray(SPECIES_FLOAT, a, i, mask);
    FloatVector vb = FloatVector.fromArray(SPECIES_FLOAT, b, i, mask);
    va.add(vb).intoArray(result, i, mask);
}
// вычисляем остаток массива
IntStream.range(upperBound, length).forEach(i -> result[i] = a[i] + b[i]);

// вариант второй
for (int i = 0; i < upperBound; i += SPECIES_FLOAT.length()) {
    FloatVector va = FloatVector.fromArray(SPECIES_FLOAT, a, i);
    FloatVector vb = FloatVector.fromArray(SPECIES_FLOAT, b, i);
    va.add(vb).intoArray(result, i);
}
// вычисляем остаток массива
IntStream.range(upperBound, length).forEach(i -> result[i] = a[i] + b[i]);

Первый вариант отличается от второго тем, что там используются masked operations. Т.е. происходит заполнение линий результата дефолтным значением.


На этом пока остановимся. Более подробное описание Vector API вполне тянет на отдельную статью.


Полностью код можно посмотреть здесь.


Методика тестирования

Берем два массива псевдослучайных чисел, выполняем некоторую операцию над элементами массивов обычным способом и используя Vector API. Сравниваем время выполнения.


Типы значений в массивах:



Размеры массивов:



Операции:



Для бенчмарков будем использовать JMH.


Результаты

Benchmark                        (arrayLength)  Mode  Cnt    Score    Error  Units
Main._11_testScalarLongSum                1000  avgt    5    0.002 ±  0.001  ms/op
Main._11_testScalarLongSum              100000  avgt    5    0.156 ±  0.026  ms/op
Main._11_testScalarLongSum           150000000  avgt    5  337.277 ± 39.361  ms/op
Main._12_testVectorApiLongSum             1000  avgt    5    0.001 ±  0.001  ms/op
Main._12_testVectorApiLongSum           100000  avgt    5    0.079 ±  0.008  ms/op
Main._12_testVectorApiLongSum        150000000  avgt    5  263.030 ± 33.013  ms/op
Main._13_testScalarDoubleSum              1000  avgt    5    0.001 ±  0.001  ms/op
Main._13_testScalarDoubleSum            100000  avgt    5    0.079 ±  0.007  ms/op
Main._13_testScalarDoubleSum         150000000  avgt    5  285.097 ± 25.093  ms/op
Main._14_testVectorApiDoubleSum           1000  avgt    5    0.001 ±  0.001  ms/op
Main._14_testVectorApiDoubleSum         100000  avgt    5    0.079 ±  0.010  ms/op
Main._14_testVectorApiDoubleSum      150000000  avgt    5  276.653 ±  4.322  ms/op
Main._21_testScalarLongMul                1000  avgt    5    0.002 ±  0.001  ms/op
Main._21_testScalarLongMul              100000  avgt    5    0.140 ±  0.005  ms/op
Main._21_testScalarLongMul           150000000  avgt    5  315.138 ± 11.634  ms/op
Main._22_testVectorApiLongMul             1000  avgt    5    0.001 ±  0.001  ms/op
Main._22_testVectorApiLongMul           100000  avgt    5    0.098 ±  0.007  ms/op
Main._22_testVectorApiLongMul        150000000  avgt    5  270.896 ±  7.999  ms/op
Main._23_testScalarDoubleMul              1000  avgt    5    0.001 ±  0.001  ms/op
Main._23_testScalarDoubleMul            100000  avgt    5    0.077 ±  0.009  ms/op
Main._23_testScalarDoubleMul         150000000  avgt    5  283.702 ± 11.657  ms/op
Main._24_testVectorApiDoubleMul           1000  avgt    5    0.001 ±  0.001  ms/op
Main._24_testVectorApiDoubleMul         100000  avgt    5    0.079 ±  0.012  ms/op
Main._24_testVectorApiDoubleMul      150000000  avgt    5  277.350 ±  7.887  ms/op
Main._31_testScalarLongComp               1000  avgt    5    0.002 ±  0.001  ms/op
Main._31_testScalarLongComp             100000  avgt    5    0.175 ±  0.002  ms/op
Main._31_testScalarLongComp          150000000  avgt    5  343.114 ± 13.517  ms/op
Main._32_testVectorApiLongComp            1000  avgt    5    0.001 ±  0.001  ms/op
Main._32_testVectorApiLongComp          100000  avgt    5    0.096 ±  0.008  ms/op
Main._32_testVectorApiLongComp       150000000  avgt    5  271.629 ±  6.572  ms/op
Main._33_testScalarFloatComp              1000  avgt    5   ≈ 10⁻³           ms/op
Main._33_testScalarFloatComp            100000  avgt    5    0.041 ±  0.014  ms/op
Main._33_testScalarFloatComp         300000000  avgt    5  326.509 ±  3.468  ms/op
Main._34_testVectorApiFloatComp           1000  avgt    5    0.001 ±  0.001  ms/op
Main._34_testVectorApiFloatComp         100000  avgt    5    0.048 ±  0.001  ms/op
Main._34_testVectorApiFloatComp      300000000  avgt    5  276.832 ±  6.612  ms/op


Выводы

Признаюсь, изначально я предполагал заметно более существенный прирост скорости (в некоторых источниках читал про вплоть до x16).


Но, JVM уже умеет авто-векторизацию в определенных случаях.


Hotspot supports some of x86 SIMD instructions
Automatic vectorization of Java code
Superword optimizations in HotSpot C2 compiler to derive SIMD code from sequential code

https://cr.openjdk.java.net/~vlivanov/talks/2019_CodeOne_MTE_Vectors.pdf


The compiler takes in standard Java bytecode and automatically determines which part can be transformed to vector instructions. Common Java environments like OpenJDK or Oracle's Java can produce vectorized machine code.

http://daniel-strecker.com/blog/2020-01-14_auto_vectorization_in_java/


Именно поэтому разница не такая впечатляющая.


Vector API позволяет явно использовать SIMD, и в некоторых специфических случаях (например, когда JVM не может полностью или оптимально произвести векторизацию) достичь заметного ускорения.


В большинстве остальных случаев вполне можно довериться JVM.


Ссылки

https://blogs.oracle.com/javamagazine/post/java-vector-api-simd
https://docs.oracle.com/en/java/javase/18/docs/api/jdk.incubator.vector/jdk/incubator/vector/package-summary.html

@isden
28.07.2022 11:57 UTC
Первоисточник

Комментарии

@dyadyaSerezha
28.07.2022 09:52 UTC
-1

FloatVector.SPECIES_128 - странное название. Почему не FloatVector.128_bit?

Далее, почему final SPECIES_FLOAT - большими буквами, а другие final - нет?

@karambaso
28.07.2022 10:00 UTC
-1

Унылое наследие пишущих на си. Сначала все эти фокусы с видеокартами проделывают именно на си, потому что проще, а потом, когда выгода темы становится очевидной, все остальные бросаются внедрять у себя копию. Именно копию. Потому что с нуля считается некомильфо (и в большинстве случаев это верно). Ну а с копией приходит и унылый стиль из языка, где нет энумов (или зубры ими никогда не пользуются), где есть традиции всё писать большими буквfми через подчёркивание, ну и где много чего вообще странного с точки зрения Java.

@isden
28.07.2022 11:57 UTC
0
FloatVector.SPECIES_128 — странное название. Почему не FloatVector.128_bit?

https://docs.oracle.com/en/java/javase/18/docs/api/jdk.incubator.vector/jdk/incubator/vector/FloatVector.html#SPECIES_128


Далее, почему final SPECIES_FLOAT — большими буквами, а другие final — нет?

А почему бы и нет? У вас какие-то принципиальные возражения?

28.07.2022 15:59 UTC
0

Ладно, может, там много всяких вариантов для 128 бит и термин species уже устоялся.

А почему бы и нет? У вас какие-то принципиальные возражения?

Только одно - единообразие в именовании.

28.07.2022 18:07 UTC
0

Исторически сложилось так, что в Java нет такого понятия как константа (а я подозреваю что именно это вы имеете в виду, говоря про использование uppercase).
Общепринято (еще раз, это не требование) использовать модификаторы static final для обозначения "константы".
Как вы можете заметить, в данном случае есть только final.
И использовал uppercase для SPECIES_FLOAT я умышленно.

@aleksandy
29.07.2022 04:54 UTC
+1

Почему не FloatVector.128_bit

Как минимум потому, что спецификация языка прямо запрещает использовать идентификаторы, начинающиеся не с Java-символа.

@YuryB
24.09.2022 12:00 UTC
0

Именно поэтому разница не такая впечатляющая.

разница скорее из-за того, что пока там ещё не всё готово. переписал https://github.com/Mark-Kovalyov/CardRaytracerBenchmark на это апи и вместо 10 сек код выполняется 40. что под 17 что 19-ой java, хотя в последней обещали какие-то улучшения. в принципе надо учитывать, что процессор супер скалаярный, он и без simd умеет умножать/складывать параллельно, по 3-4 инструкции минимум, а тут ещё какая-то обвязка в виде объектов, которые надо чистить. хотя конечно gc в моём случаи работал наверно только 1 сек из 40. но всё равно, не на такие результаты я расчитывал, переделывая код, причём он на 100% использует новое api.

@isden
24.09.2022 12:16 UTC
0
и вместо 10 сек код выполняется 40

У меня было похожее замедление при бенчмарке совсем небольших массивов (в районе 1-2 сотен).
Подозреваю, что оверхед на загрузку модуля и вызов апи таким вот способом довольно дорогой.


По поводу улучшений в 19:


Improvements to the API proposed for JDK 19 include enhancements to load and store vectors to and from MemorySegments, as defined by the Foreign Function and Memory API preview. JDK 19 would also add two cross-lane vector operations, compress and expand, together with a complementary vector mask compress operation.
In another addition to the vector API, bitwise integral lanewise operations would be expanded, including operations such counting the number of one bits, reversing the order of bits, and compressing and expanding bits.

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