Vector API в Java: краткий обзор и тестирование
https://t.me/javatgНекоторое время назад попалась на глаза статья про Vector API в Java.
Прочитал, заинтересовался. Наконец, недавно дошли руки посмотреть, что же это такое и как работает.
Результаты немного неоднозначные.
Краткое описание
Итак, Vector API в Java сейчас — это модуль, который предоставляет возможность выполнять векторные вычисления, ускоряемые аппаратно. Является частью Project Panama.
В данный момент (Java 18) модуль находится в стадии третьего инкубатора.
Ключевая особенность — использование SIMD (single instruction, multiple data) — т.е. выполнение математической операции над несколькими скалярными значениями ( == вектором ) одновременно, что в теории может дать ускорение вычислений.
Вот как это выглядит в базовом случае:
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
определяет тип вектора:
FloatVector
— описывает тип значения вектора (линии) —float
(32 бита)SPECIES_128
— определяет размер вектора (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. Сравниваем время выполнения.
Типы значений в массивах:
long
(на самом делеint
, преобразованный вlong
)double
float
Размеры массивов:
- 1000
- 100_000
- 150_000_000
- 300_000_000 (только для
float
)
Операции:
a + b
-> отдельно массивыlong
иdouble
a * b
-> отдельно массивыlong
иdouble
(a + b) * 5
-> массивыlong
(a * b) + 0.4
-> массивыfloat
Для бенчмарков будем использовать 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
- На массивах небольшого размера разница в скорости плюс-минус на уровне погрешности измерения (на более мелких массивах, в районе сотни элементов, вычисления с использованием Vector API проходят заметно медленнее, чем скалярные).
- Преимущество в скорости на больших массивах определеннно есть, но не очень значительное.
Выводы
Признаюсь, изначально я предполагал заметно более существенный прирост скорости (в некоторых источниках читал про вплоть до 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