Некоторое время назад попалась на глаза статья про 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 определяет тип вектора:


  • 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
https://docs.oracle.com/en/java/javase/18/docs/api/jdk.incubator.vector/jdk/incubator/vector/package-summary.html

Комментарии (6)


  1. dyadyaSerezha
    28.07.2022 12:52
    -1

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

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


    1. karambaso
      28.07.2022 13:00
      -1

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


    1. isden Автор
      28.07.2022 14:57

      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 — нет?

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


      1. dyadyaSerezha
        28.07.2022 18:59

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

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

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


        1. isden Автор
          28.07.2022 21:07

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


    1. aleksandy
      29.07.2022 07:54
      +1

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

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