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

JEP 426: Vector API (Fourth Incubator)

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

ОтветственныйPaul Sandoz
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск19
Компонентcore-libs
Обсуждениеpanama dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
Связан сJEP 438: Vector API (Fifth Incubator)
JEP 417: Vector API (Third Incubator)
РецензентыJohn Rose, Vladimir Kozlov
ОдобренJohn Rose
Создан2022/01/18 19:36
Обновлён2023/05/12 15:35
Задача8280173

Аннотация

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

История

Vector API был впервые предложен в JEP 338 и интегрирован в JDK 16 как API в статусе Incubator. Второй раунд в статусе Incubator был предложен в JEP 414 и интегрирован в JDK 17. Третий раунд в статусе Incubator был предложен в JEP 417 и интегрирован в JDK 18.

Здесь мы предлагаем внести улучшения по итогам отзывов, а также улучшения производительности и другие существенные улучшения реализации. Среди заметных изменений:

  • Расширить API для загрузки векторов из MemorySegment и сохранения в них, как определено в JEP 424: Foreign Function & Memory (FFM) API (Preview (предварительная версия)). FFM API достаточно зрелый, чтобы мы могли без опасений добавить эту зависимость в Vector API. Мы удалим эквивалентные точки API, работающие с byte[] и ByteBuffer, поскольку MemorySegment можно получить для любого из них. Использование MemorySegment позволит создавать гипервыровненные области памяти, выровненные по длине вектора в байтах. На некоторых архитектурах такое выравнивание даёт более высокую производительность при загрузке и сохранении векторов.

  • Добавить две новые межполосные векторные операции — compress и обратную ей expand, а также дополняющую их операцию compress для векторной маски. Векторная операция compress отображает полосы исходного вектора, выбранные маской, в целевой вектор в порядке полос; операция expand делает обратное. Операция compress полезна для фильтрации результатов запросов: например, она может выбрать все ненулевые целочисленные элементы исходного вектора и записать их подряд, в порядке полос исходного вектора, в целевой вектор.

  • Расширить набор поддерживаемых поэлементных побитовых операций над целыми числами, добавив:

    • подсчёт числа единичных битов,
    • подсчёт числа ведущих нулевых битов,
    • подсчёт числа завершающих нулевых битов,
    • обращение порядка битов,
    • обращение порядка байтов и
    • сжатие и расширение битов.

    Две последние операции аналогичны межполосным операциям compress и expand, но отображают биты, а не целые полосы. (Подробнее о побитовых compress и expand см. §7-4 и §7-5 книги Hacker's Delight Генри С. Уоррена, Addison-Wesley, 2013.)

    Одновременно с этим JEP, но отдельно от него, мы добавим в обёрточные типы примитивов Integer и Long методы для побитовых скалярных операций compress и expand, которые полезны и сами по себе. Побитовые векторные операции compress и expand мы специфицируем через эти новые скалярные методы.

Цели

  • Понятный и лаконичный 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 должна плавно деградировать и продолжать работать. При этом могут выдаваться предупреждения, если векторное вычисление не удаётся эффективно скомпилировать в векторные инструкции. На платформах без векторов плавная деградация даст код, сопоставимый по скорости с циклами, развёрнутыми вручную, где коэффициент развёртывания равен числу полос в выбранном векторе.

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

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

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

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

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

Мотивация

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

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

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

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

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

Описание

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

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

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

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

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

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

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

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

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

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

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

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

Для поддержки потока управления некоторые векторные операции могут принимать маски, представленные публичным абстрактным классом 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 предпочтительный вид, форма которого оптимальна для текущей архитектуры. Мы сохраняем его в поле 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, поэтому она функциональна, но не оптимальна. Вторая определяет встроенные (intrinsic) векторные операции для компилятора времени выполнения HotSpot C2, чтобы он мог компилировать векторные вычисления в соответствующие аппаратные регистры и векторные инструкции, когда они доступны.

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

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

Интринсики 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 с именованными процедурами-заглушками. Компилятор C2 генерирует код, вызывающий соответствующую процедуру-заглушку в зависимости от операции и вида вектора (т. е. типа элементов и формы).

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

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

  • Как упоминалось выше, в конечном счёте мы рассчитываем объявить векторные классы как primitive classes. Кроме того, мы рассчитываем использовать обобщённую специализацию primitive classes из проекта Valhalla, чтобы экземпляры Vector<E> могли быть примитивными значениями, конкретные типы которых являются примитивными типами. Это упростит оптимизацию и запись векторных вычислений. Подтипы Vector<E> для конкретных типов, например IntVector, могут оказаться ненужными, когда появится обобщённая специализация над primitive classes. Мы намерены развивать API в статусе Incubator на протяжении нескольких выпусков и адаптировать его по мере появления primitive classes и связанных с ними средств.

  • Мы рассчитываем улучшить реализацию, чтобы лучше оптимизировать циклы, содержащие векторизованный код, и в целом постепенно повышать производительность со временем.

  • Мы также рассчитываем улучшить комбинаторные модульные тесты, чтобы они проверяли, что 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 в конечном счёте введёт более мощные обобщённые типы, текущее решение будет выглядеть неудачным и, скорее всего, потребует изменения. Мы предполагаем, что такие изменения будут возможны без чрезмерной обратной несовместимости.