openjdk.ruOpenJDK на русском

JEP 489: Vector API (Ninth Incubator)

Vector API, девятая версия Incubator (инкубационный модуль)

ОтветственныйPaul Sandoz
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск24
Компонентcore-libs
Обсуждениеpanama dash dev at openjdk dot org
ТрудоёмкостьXS
ДлительностьXS
Связан сJEP 469: Vector API (Eighth Incubator)
JEP 508: Vector API (Tenth Incubator)
РецензентыJatin Bhateja, Sandhya Viswanathan, Vladimir Ivanov
ОдобренJohn Rose
Создан2024/09/24 19:20
Обновлён2025/04/10 17:49
Задача8340841

Аннотация

Ввести API для выражения векторных вычислений, которые во время выполнения надёжно компилируются в оптимальные векторные инструкции на поддерживаемых архитектурах процессоров, и тем самым достичь производительности, превосходящей производительность эквивалентных скалярных вычислений.

История

Впервые мы предложили Vector API в JEP 338; он был интегрирован в JDK 16 как API в статусе Incubator. Дальнейшие этапы в статусе Incubator мы предлагали в JEP 414 (интегрирован в JDK 17), JEP 417 (JDK 18), JEP 426 (JDK 19), JEP 438 (JDK 20), JEP 448 (JDK 21), JEP 460 (JDK 22) и JEP 469 (JDK 23).

Здесь мы предлагаем снова выпустить API в статусе Incubator в JDK 24 со следующими заметными изменениями:

  • Новый вариант межполосной операции selectFrom принимает два входных вектора, которые служат таблицей поиска. Он дополняет соответствующую существующую операцию rearrange.

  • Межполосные операции selectFrom и rearrange теперь не проверяют индексы на выход за границы, а циклически оборачивают их. Вместе с другими улучшениями реализации это сделало данные операции значительно быстрее. В результате Vector API может выражать эффективные SIMD-алгоритмы, использующие поиск по таблицам.

  • Трансцендентные и тригонометрические пополосные операции на ARM и RISC-V теперь реализованы через интринсики, которые вызывают библиотеку SIMD Library for Evaluating Elementary Functions (SLEEF).

  • Новый класс Float16, основанный на значениях (value-based), представляет 16-битные числа с плавающей точкой в формате IEEE 754 binary16. В будущем мы намерены автоматически векторизовать операции над Float16 на поддерживающем это оборудовании, расширить Vector API для поддержки элементов, являющихся значениями Float16, и в конечном счёте перевести Float16 в категорию Value Classes (классы-значения), когда проект Valhalla станет доступен.

  • Целочисленные арифметические пополосные операции теперь включают:

    • Беззнаковое сложение и вычитание с насыщением,
    • Знаковое сложение и вычитание с насыщением и
    • Беззнаковые максимум и минимум.

    Эквивалентные скалярные методы объявлены в новом классе VectorMath, и мы специфицируем пополосные операции через эти новые скалярные методы.

Vector API будет оставаться в статусе Incubator, пока необходимые возможности проекта Valhalla не станут доступны как возможности в статусе Preview (предварительная версия). Тогда мы адаптируем Vector API и его реализацию для их использования и переведём Vector API из статуса Incubator в статус Preview.

Цели

  • Ясный и лаконичный API — API должен позволять ясно и лаконично выражать широкий спектр векторных вычислений, состоящих из последовательностей векторных операций, скомпонованных внутри циклов и, возможно, с управляющей логикой. Должна быть возможность выразить вычисление, обобщённое по размеру вектора, то есть по числу полос в векторе, чтобы такие вычисления были переносимы между оборудованием, поддерживающим разные размеры векторов.

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

  • Надёжная компиляция во время выполнения и производительность на архитектурах x64 и AArch64 — На подходящих архитектурах x64 среда выполнения Java, а именно компилятор HotSpot C2, должна компилировать векторные операции в соответствующие эффективные и производительные векторные инструкции, например поддерживаемые Streaming SIMD Extensions (SSE) и Advanced Vector Extensions (AVX). Разработчики должны быть уверены, что выраженные ими векторные операции будут надёжно и точно отображаться на соответствующие векторные инструкции. Аналогично, на подходящих архитектурах ARM AArch64 C2 будет компилировать векторные операции в векторные инструкции, поддерживаемые NEON и SVE.

  • Плавная деградация — Иногда векторное вычисление невозможно полностью выразить во время выполнения в виде последовательности векторных инструкций, например потому, что архитектура не поддерживает некоторые из требуемых инструкций. В таких случаях реализация Vector API должна плавно деградировать и продолжать работать. Это может включать выдачу предупреждений, если векторное вычисление не удаётся эффективно скомпилировать в векторные инструкции. На платформах без векторов плавная деградация даст код, сопоставимый по производительности с вручную развёрнутыми циклами, где коэффициент развёртки равен числу полос в выбранном векторе.

  • Согласованность с проектом Valhalla — Долгосрочная цель Vector API — использовать улучшения объектной модели Java, которые вносит проект Valhalla. В первую очередь это будет означать превращение текущих классов Vector API, основанных на значениях (value-based), в Value Classes, чтобы программы могли работать с Value Objects (объекты-значения), т. е. с экземплярами классов, лишёнными Identity (идентичность объекта). Подробнее см. разделы о компиляции во время выполнения и о дальнейшей работе.

Что не является целью

  • Улучшение существующего алгоритма автовекторизации в HotSpot не является целью.

  • Поддержка векторных инструкций на архитектурах процессоров, отличных от x64 и AArch64, не является целью. Однако важно подчеркнуть, как сказано в целях, что API не должен исключать такие реализации.

  • Поддержка компилятора C1 не является целью.

  • Не является целью гарантировать поддержку строгих вычислений с плавающей точкой, которых платформа Java требует для скалярных операций. Результаты операций с плавающей точкой над скалярами с плавающей точкой могут отличаться от результатов эквивалентных операций с плавающей точкой над векторами скаляров с плавающей точкой. Все отклонения будут чётко задокументированы. Этот пункт не исключает возможности выражать или контролировать желаемую точность или воспроизводимость векторных вычислений с плавающей точкой.

Мотивация

Векторное вычисление состоит из последовательности операций над векторами. Вектор представляет собой (как правило) фиксированную последовательность скалярных значений, число которых соответствует числу аппаратно определённых векторных полос. Бинарная операция, применённая к двум векторам с одинаковым числом полос, для каждой полосы применяет эквивалентную скалярную операцию к двум соответствующим скалярным значениям из каждого вектора. Обычно это называют Single Instruction Multiple Data (SIMD).

Векторные операции выражают определённую степень параллелизма, благодаря которой за один такт процессора выполняется больше работы, и поэтому могут давать значительный прирост производительности. Например, если есть два вектора, каждый из которых содержит последовательность из восьми целых чисел (то есть восемь дорожек), их можно сложить одной аппаратной инструкцией. Инструкция векторного сложения обрабатывает шестнадцать целых чисел, выполняя восемь целочисленных сложений, за то время, которое обычно уходит на обработку двух целых чисел с одним целочисленным сложением.

HotSpot уже поддерживает автовекторизацию, которая преобразует скалярные операции в суперсловные (superword) операции, а те затем отображаются на векторные инструкции. Набор преобразуемых скалярных операций ограничен и, кроме того, чувствителен к изменениям формы кода. Более того, может использоваться лишь часть доступных векторных инструкций, что ограничивает производительность сгенерированного кода.

Сегодня разработчику, который хочет писать скалярные операции, надёжно преобразуемые в суперсловные операции, нужно понимать алгоритм автовекторизации HotSpot и его ограничения, чтобы добиться надёжной и устойчивой производительности. В некоторых случаях написать преобразуемые скалярные операции может оказаться невозможно. Например, HotSpot не преобразует простые скалярные операции для вычисления хэш-кода массива (методы Arrays::hashCode) и не может автоматически векторизовать код лексикографического сравнения двух массивов (поэтому мы добавили интринсик для лексикографического сравнения).

Vector API призван улучшить ситуацию: он даёт способ писать сложные векторные алгоритмы на Java с использованием существующего автовекторизатора HotSpot, но с пользовательской моделью, которая делает векторизацию гораздо более предсказуемой и надёжной. Написанные вручную векторные циклы могут выражать высокопроизводительные алгоритмы, такие как векторизованный hashCode или специализированные сравнения массивов, которые автовекторизатор может так никогда и не оптимизировать. Этот явный векторный API может принести пользу многим областям, включая машинное обучение, линейную алгебру, криптографию, финансы и код внутри самого JDK.

Описание

Вектор представлен абстрактным классом Vector<E>. Переменная типа E конкретизируется упакованным (boxed) типом скалярных примитивных целочисленных или вещественных типов элементов, которые охватывает вектор. У вектора также есть форма (shape), определяющая размер вектора в битах. Форма вектора определяет, как экземпляр Vector<E> отображается на аппаратный векторный регистр при компиляции векторных вычислений компилятором HotSpot C2. Длина вектора, т. е. число полос или элементов, равна размеру вектора, делённому на размер элемента.

Поддерживаемый набор типов элементов (E): Byte, Short, Integer, Long, Float и Double, что соответствует скалярным примитивным типам byte, short, int, long, float и double соответственно.

Поддерживаемый набор форм соответствует размерам векторов 64, 128, 256 и 512 бит, а также max бит. Форма размером 512 бит может упаковать значения byte в 64 полосы или значения int в 16 полос, и вектор такой формы может обрабатывать 64 значения byte или 16 значений int за раз. Форма max бит поддерживает максимальный размер вектора текущей архитектуры. Это обеспечивает поддержку платформы ARM SVE, реализации которой могут поддерживать любой фиксированный размер от 128 до 2048 бит с шагом 128 бит.

Мы считаем, что эти простые формы достаточно универсальны, чтобы быть полезными на всех актуальных платформах. Однако, экспериментируя с будущими платформами, пока этот API находится в статусе Incubator, мы можем дополнительно изменить устройство параметра формы. Такая работа не входит в начальный объём этого проекта, но эти возможности отчасти определяют нынешнюю роль форм в Vector API. (Подробнее см. раздел «Дальнейшая работа» ниже.)

Сочетание типа элементов и формы определяет вид (species) вектора, который представлен VectorSpecies<E>.

Операции над векторами делятся на поэлементные (lane-wise) и междорожечные (cross-lane).

  • Поэлементная операция параллельно применяет скалярный оператор, например сложение, к каждой дорожке одного или нескольких векторов. Поэлементная операция обычно, но не всегда, даёт вектор той же длины и формы. Поэлементные операции далее делятся на унарные, бинарные, тернарные операции, операции проверки и операции преобразования.

  • Межполосная операция применяет операцию ко всему вектору целиком. Межполосная операция даёт либо скаляр, либо вектор, возможно, другой формы. Межполосные операции дополнительно делятся на операции перестановки и операции свёртки (reduction).

Чтобы уменьшить поверхность API, мы определяем общие методы для каждого класса операций. Эти методы принимают на вход константы операторов; эти константы являются экземплярами класса VectorOperators.Operator и определены в полях static final класса VectorOperators. Для удобства мы определяем специальные методы, которые можно использовать вместо обобщённых, для некоторых распространённых полнофункциональных (full-service) операций, таких как сложение и умножение.

Некоторые операции над векторами, такие как преобразование и переинтерпретация, по своей природе меняют форму, т. е. дают векторы, формы которых отличаются от форм их входных данных. Операции, меняющие форму, в векторном вычислении могут отрицательно сказываться на переносимости и производительности. Поэтому API определяет для каждой операции, меняющей форму, где это применимо, инвариантный к форме вариант. Для наилучшей производительности разработчикам следует по возможности писать инвариантный к форме код с использованием инвариантных к форме операций. Операции, меняющие форму, помечены как таковые в спецификации API.

Класс Vector<E> объявляет набор методов для распространённых векторных операций, поддерживаемых всеми типами элементов. Для операций, специфичных для типа элементов, существует шесть абстрактных подклассов Vector<E>, по одному на каждый поддерживаемый тип элементов: ByteVector, ShortVector, IntVector, LongVector, FloatVector и DoubleVector. Эти специфичные для типа подклассы определяют дополнительные операции, привязанные к типу элементов, поскольку сигнатура метода ссылается либо на тип элементов, либо на соответствующий тип массива. Примеры таких операций — свёртка (например, суммирование всех полос в скалярное значение) и копирование элементов вектора в массив. Эти подклассы также определяют дополнительные полноценные операции, специфичные для целочисленных подтипов (например, побитовые операции, такие как логическое «или»), а также операции, специфичные для типов с плавающей точкой (например, трансцендентные математические функции, такие как возведение в степень).

С точки зрения реализации эти специфичные для типа подклассы Vector<E> дополнительно расширяются конкретными подклассами для различных форм векторов. Эти конкретные подклассы не являются публичными, поскольку нет необходимости предоставлять операции, специфичные одновременно для типов и форм. Благодаря этому поверхность API сводится к сумме аспектов, а не к их произведению. Экземпляры конкретных классов Vector получают через фабричные методы, определённые в базовом классе Vector<E> и его специфичных для типа подклассах. Эти фабрики принимают на вход вид (species) требуемого экземпляра вектора и создают различные разновидности экземпляров, например экземпляр вектора, элементы которого имеют значения по умолчанию (т. е. нулевой вектор), или экземпляр вектора, инициализированный из заданного массива.

Для управления потоком выполнения некоторые векторные операции могут принимать маски, представленные публичным абстрактным классом VectorMask<E>. Каждый элемент маски — это логическое значение, соответствующее дорожке вектора. Маска выбирает дорожки, к которым применяется операция: операция применяется, если элемент маски для дорожки равен true, а если он равен false, выполняется некоторое альтернативное действие.

Как и в случае векторов, экземпляры VectorMask<E> являются экземплярами непубличных конкретных подклассов, определённых для каждого сочетания типа элементов и длины. Экземпляр VectorMask<E>, используемый в операции, должен иметь тот же тип и ту же длину, что и экземпляры векторов, участвующие в операции. Операции сравнения векторов дают маски, которые затем можно передавать на вход другим операциям, чтобы выборочно работать с определёнными дорожками и тем самым эмулировать поток управления. Маски также можно создавать с помощью статических фабричных методов класса VectorMask<E>.

Мы ожидаем, что маски будут играть важную роль в разработке векторных вычислений, обобщённых по форме. Это ожидание основано на ключевой роли предикатных регистров, эквивалента масок, в ARM Scalable Vector Extensions и в Intel AVX-512.

На таких платформах экземпляр VectorMask<E> отображается на предикатный регистр, а операция, принимающая маску, компилируется в векторную инструкцию, принимающую предикатный регистр. На платформах, не поддерживающих предикатные регистры, применяется менее эффективный подход: экземпляр VectorMask<E> по возможности отображается на совместимый векторный регистр, а операция, принимающая маску, в общем случае составляется из эквивалентной операции без маски и операции смешивания (blend).

Для поддержки операций перестановки между дорожками некоторые векторные операции принимают перестановки (shuffles), представленные публичным абстрактным классом VectorShuffle<E>. Каждый элемент перестановки — значение int, соответствующее индексу дорожки. Перестановка — это отображение индексов дорожек, описывающее перемещение элементов дорожек из заданного вектора в результирующий вектор.

Как и в случае векторов и масок, экземпляры VectorShuffle<E> являются экземплярами непубличных конкретных подклассов, определённых для каждого сочетания типа элементов и длины. Экземпляр VectorShuffle<E>, используемый в операции, должен иметь тот же тип и ту же длину, что и экземпляры векторов, участвующие в операции.

Пример

Вот простое скалярное вычисление над элементами массивов:

void scalarComputation(float[] a, float[] b, float[] c) {
   for (int i = 0; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
   }
}

(Мы предполагаем, что массивы-аргументы имеют одинаковую длину.)

Вот эквивалентное векторное вычисление с использованием Vector API:

static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

void vectorComputation(float[] a, float[] b, float[] c) {
    int i = 0;
    int upperBound = SPECIES.loopBound(a.length);
    for (; i < upperBound; i += SPECIES.length()) {
        // FloatVector va, vb, vc;
        var va = FloatVector.fromArray(SPECIES, a, i);
        var vb = FloatVector.fromArray(SPECIES, b, i);
        var vc = va.mul(va)
                   .add(vb.mul(vb))
                   .neg();
        vc.intoArray(c, i);
    }
    for (; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}

Сначала мы получаем из FloatVector предпочтительный вид (species), форма которого оптимальна для текущей архитектуры. Мы сохраняем его в поле static final, чтобы компилятор среды выполнения рассматривал это значение как константу и поэтому мог лучше оптимизировать векторное вычисление. Затем основной цикл проходит по входным массивам с шагом, равным длине вектора, то есть длине вида. Он загружает векторы float заданного вида из массивов a и b по соответствующему индексу, последовательно выполняет арифметические операции и затем сохраняет результат в массив c. Если после последней итерации остаются элементы массивов, то результаты для этих хвостовых элементов вычисляются обычным скалярным циклом.

Эта реализация достигает оптимальной производительности на больших массивах. Компилятор HotSpot C2 генерирует на процессоре Intel x64 с поддержкой AVX машинный код, подобный следующему:

0.43%  / │  0x0000000113d43890: vmovdqu 0x10(%r8,%rbx,4),%ymm0
  7.38%  │ │  0x0000000113d43897: vmovdqu 0x10(%r10,%rbx,4),%ymm1
  8.70%  │ │  0x0000000113d4389e: vmulps %ymm0,%ymm0,%ymm0
  5.60%  │ │  0x0000000113d438a2: vmulps %ymm1,%ymm1,%ymm1
 13.16%  │ │  0x0000000113d438a6: vaddps %ymm0,%ymm1,%ymm0
 21.86%  │ │  0x0000000113d438aa: vxorps -0x7ad76b2(%rip),%ymm0,%ymm0
  7.66%  │ │  0x0000000113d438b2: vmovdqu %ymm0,0x10(%r9,%rbx,4)
 26.20%  │ │  0x0000000113d438b9: add    $0x8,%ebx
  6.44%  │ │  0x0000000113d438bc: cmp    %r11d,%ebx
         \ │  0x0000000113d438bf: jl     0x0000000113d43890

Это вывод микробенчмарка JMH для приведённого выше кода, использующего прототип Vector API и реализацию из ветки vectorIntrinsics репозитория разработки проекта Panama. Эти горячие участки сгенерированного машинного кода явно транслируются в векторные регистры и векторные инструкции. Мы отключили раскрутку циклов (с помощью опции HotSpot -XX:LoopUnrollLimit=0), чтобы трансляция была нагляднее; иначе HotSpot раскрутил бы этот код с помощью существующих оптимизаций циклов C2. Все выделения памяти под объекты Java устранены.

(HotSpot способен автоматически векторизовать скалярное вычисление в этом конкретном примере и сгенерирует похожую последовательность векторных инструкций. Главное отличие в том, что автовекторизатор генерирует для умножения на -1.0f векторную инструкцию умножения, тогда как реализация Vector API генерирует векторную инструкцию XOR, которая инвертирует знаковый бит. Однако главная цель этого примера — представить Vector API и показать, как его реализация генерирует векторные инструкции, а не сравнить его с автовекторизатором.)

На платформах, поддерживающих предикатные регистры, приведённый выше пример можно было бы записать проще, без скалярного цикла для обработки хвостовых элементов, сохранив при этом оптимальную производительность:

void vectorComputation(float[] a, float[] b, float[] c) {
    for (int i = 0; i < a.length; i += SPECIES.length()) {
        // VectorMask<Float>  m;
        var m = SPECIES.indexInRange(i, a.length);
        // FloatVector va, vb, vc;
        var va = FloatVector.fromArray(SPECIES, a, i, m);
        var vb = FloatVector.fromArray(SPECIES, b, i, m);
        var vc = va.mul(va)
                   .add(vb.mul(vb))
                   .neg();
        vc.intoArray(c, i, m);
    }
}

В теле цикла мы получаем зависящую от цикла маску, которая передаётся в операции загрузки и сохранения. Когда i < SPECIES.loopBound(a.length), маска m объявляет все дорожки установленными. На последней итерации цикла, когда SPECIES.loopBound(a.length) <= i < a.length и (a.length - i) <= SPECIES.length(), маска может объявлять неустановленным завершающий участок дорожек. Операции загрузки и сохранения не будут выбрасывать исключения выхода за границы, поскольку маска не допускает обращения к массиву за пределами его длины.

Мы бы предпочли, чтобы разработчики писали в приведённом выше стиле для всех поддерживаемых платформ и получали оптимальную производительность, но сегодня на платформах без предикатных регистров такой подход не оптимален. Теоретически компилятор C2 можно было бы улучшить так, чтобы он преобразовывал цикл, отделяя последнюю итерацию и убирая маску из тела цикла. Это остаётся областью для дальнейшего исследования.

Компиляция во время выполнения

У Vector API две реализации. Первая реализует операции на Java, поэтому она работоспособна, но не оптимальна. Вторая определяет интринсики векторных операций для компилятора среды выполнения HotSpot C2, чтобы он мог компилировать векторные вычисления в соответствующие аппаратные регистры и векторные инструкции, если они доступны.

Чтобы избежать взрывного роста числа интринсиков C2, мы определяем обобщённые интринсики, соответствующие различным видам операций, таким как унарные, бинарные, преобразования и т. д., которые принимают параметр, описывающий конкретную выполняемую операцию. Интринсификацию всего API обеспечивают около двадцати пяти новых интринсиков.

В конечном счёте мы рассчитываем объявить векторные классы как Value Classes, как предложено проектом Valhalla (JEP 401). А пока Vector<E> и его подклассы считаются value-based-классами, поэтому операций, зависящих от Identity, над их экземплярами следует избегать. Хотя экземпляры векторов абстрактно состоят из элементов в дорожках, C2 не скаляризует эти элементы — значение вектора рассматривается как единое целое, подобно int или long, и отображается на векторный регистр соответствующего размера. C2 обрабатывает экземпляры векторов особым образом, чтобы обойти ограничения escape-анализа и избежать упаковки (boxing). В будущем мы согласуем эту особую обработку с Value Objects проекта Valhalla.

Интринсики Intel SVML для трансцендентных операций

Vector API поддерживает трансцендентные и тригонометрические поэлементные (lanewise) операции над векторами с плавающей точкой. На x64 мы используем библиотеку Intel Short Vector Math Library (SVML), чтобы предоставить оптимизированные реализации таких операций в виде интринсиков. Интринсики имеют те же численные свойства, что и соответствующие скалярные операции, определённые в java.lang.Math.

Исходные файлы на ассемблере для операций SVML находятся в исходном коде модуля jdk.incubator.vector, в каталогах для конкретных ОС. Процесс сборки JDK компилирует эти исходные файлы для целевой операционной системы в специальную разделяемую библиотеку SVML. Эта библиотека довольно велика: её размер чуть меньше мегабайта. Если образ JDK, собранный с помощью jlink, не включает модуль jdk.incubator.vector, то библиотека SVML в образ не копируется.

Сейчас реализация поддерживает только Linux и Windows. Поддержку macOS мы рассмотрим позже, поскольку подготовка исходных файлов на ассемблере с необходимыми директивами требует немалого объёма работы.

Среда выполнения HotSpot попытается загрузить библиотеку SVML и, если она есть, связать операции из библиотеки SVML с именованными подпрограммами-заглушками (stub routines). Компилятор C2 генерирует код, вызывающий соответствующую подпрограмму-заглушку в зависимости от операции и вида вектора (то есть типа элементов и формы).

Если в будущем проект Panama расширит поддержку нативных соглашений о вызовах так, чтобы поддерживать векторные значения, то реализация Vector API, возможно, сможет загружать библиотеку SVML из внешнего источника. Если такой подход не скажется на производительности, то включать SVML в виде исходного кода и встраивать её в сборку JDK больше не потребуется. До тех пор мы считаем описанный выше подход приемлемым, учитывая возможный выигрыш в производительности.

Дальнейшая работа

  • Как упоминалось выше, в конечном счёте мы рассчитываем объявить векторные классы как Value Classes. О текущей работе по согласованию Vector API с Valhalla см. ветку lworld+vector репозитория кода проекта Valhalla. Кроме того, мы рассчитываем использовать обобщённую специализацию Value Classes из проекта Valhalla, чтобы экземпляры Vector<E> были Value Objects, где E — примитивный класс, такой как int, а не его класс-обёртка Integer. Подтипы Vector<E> для конкретных типов, такие как IntVector, могут стать ненужными, когда появится обобщённая специализация по примитивным классам.
  • Мы можем добавить поддержку векторов из значений IEEE с плавающей точкой формата binary16 (значений float16). Это тоже зависит от проекта Valhalla: потребуется представить значение float16 как Value Objects с оптимизированным размещением в массивах и полях и улучшить реализацию Vector API так, чтобы она использовала аппаратные векторные инструкции для векторов из значений float16. Исследовательскую работу см. в ветке vectorIntrinsics+fp16 репозитория кода Vector API проекта Panama.
  • Мы рассчитываем улучшить реализацию, чтобы лучше оптимизировать циклы, содержащие векторизованный код, и в целом постепенно повышать производительность со временем.

  • Мы также рассчитываем улучшить комбинаторные модульные тесты, чтобы они проверяли, что C2 генерирует аппаратные векторные инструкции. Сейчас модульные тесты без проверки предполагают, что многократного выполнения достаточно, чтобы C2 сгенерировал аппаратные векторные инструкции. Мы изучим возможность использовать IR Test Framework компилятора C2, чтобы кросс-платформенно проверять наличие векторных узлов в IR-графе (например, с помощью сопоставления с регулярными выражениями). Если этот подход окажется проблематичным, мы можем рассмотреть простейший подход, использующий флаг -XX:+TraceNewVectors, доступный только в непродуктовых сборках, для вывода векторных узлов.

  • Мы оценим возможность определения синтетических форм векторов, чтобы лучше управлять раскруткой циклов и матричными операциями, и рассмотрим подходящую поддержку алгоритмов сортировки и синтаксического разбора. (Подробнее см. эту презентацию.)

Альтернативы

Альтернативный подход — автовекторизация в HotSpot, но она потребовала бы значительной работы. Более того, по сравнению с Vector API она всё равно была бы хрупкой и ограниченной, поскольку автовекторизацию при сложном потоке управления выполнить очень трудно.

В целом, даже после десятилетий исследований — особенно для циклов по массивам в FORTRAN и C — похоже, что автовекторизация скалярного кода не является надёжной тактикой оптимизации произвольных циклов, написанных пользователем, если только пользователь не уделяет необычайно пристального внимания негласным договорённостям о том, какие именно циклы компилятор готов автовекторизовать. Слишком легко написать цикл, который не удаётся автовекторизовать по причине, которую не может обнаружить ни один человек, читающий код. Годы работы над автовекторизацией, даже в HotSpot, оставили нам множество механизмов оптимизации, которые срабатывают лишь в особых случаях. Мы хотим пользоваться этими механизмами чаще!

Тестирование

Мы разработаем комбинаторные модульные тесты, чтобы обеспечить покрытие всех операций для всех поддерживаемых типов и форм на различных наборах данных.

Мы также разработаем тесты производительности, чтобы убедиться, что цели по производительности достигнуты и векторные вычисления эффективно отображаются на векторные инструкции. Скорее всего, это будут микробенчмарки JMH, но потребуются и более реалистичные примеры полезных алгоритмов. Такие тесты поначалу могут находиться в отдельном репозитории проекта. Перед интеграцией в основной репозиторий, вероятно, потребуется их отбор, учитывая количество тестов и способ их генерации.

Риски и допущения

  • Существует риск, что API окажется смещён в сторону функциональности SIMD, поддерживаемой на архитектурах x64, но он снижается благодаря поддержке AArch64. Это касается в основном явно фиксированного набора поддерживаемых форм, который мешает писать алгоритмы в стиле, обобщённом по форме. Большинство остальных операций Vector API, по нашему мнению, склоняют к написанию переносимых алгоритмов. Чтобы снизить этот риск, мы будем учитывать и другие архитектуры, в частности архитектуру ARM Scalar Vector Extension, модель программирования которой динамически подстраивается под единственную фиксированную форму, поддерживаемую оборудованием. Мы приветствуем и поощряем участие в этой работе участников OpenJDK, работающих над ARM-специфичными частями HotSpot.

  • Vector API использует типы-обёртки (например, Integer) в качестве заместителей примитивных типов (например, int). Это решение вынужденное: его диктуют текущие ограничения обобщённых типов Java, которые плохо сочетаются с примитивными типами. Когда проект Valhalla со временем добавит более мощные обобщённые типы, текущее решение будет выглядеть неудобным и, вероятно, потребует изменений. Мы предполагаем, что такие изменения можно будет внести без чрезмерной потери обратной совместимости.