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

JEP 508: Vector API (Tenth Incubator)

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

ОтветственныйIan Graves
ТипFeature
ОбластьJDK
СтатусClosed / Delivered
Выпуск25
Компонентcore-libs
Обсуждениеpanama dash dev at openjdk dot org
ТрудоёмкостьXS
ДлительностьXS
Связан сJEP 489: Vector API (Ninth Incubator)
JEP 529: Vector API (Eleventh Incubator)
РецензентыJatin Bhateja, Sandhya Viswanathan, Vladimir Ivanov
ОдобренPaul Sandoz
Создан2025/03/31 18:19
Обновлён2025/10/01 17:55
Задача8353296

Аннотация

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

История

Впервые мы предложили 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) и JEP 489 (JDK 24).

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

  • VectorShuffle теперь поддерживает чтение из MemorySegment и запись в него.

  • Теперь реализация подключает нативные библиотеки математических функций через Foreign Function & Memory API (JEP 454), а не через собственный код на C++ внутри HotSpot JVM, что упрощает сопровождение.

  • Операции сложения, вычитания, деления, умножения, извлечения квадратного корня и совмещённого умножения-сложения над значениями Float16 теперь автоматически векторизуются на x64 CPU, которые это поддерживают.

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

Цели

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

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

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

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

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

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

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

  • Реализация поддержки векторных инструкций на процессорных архитектурах, отличных от x64 и AArch64, не является целью, хотя API не должен исключать такие реализации. Другие участники уже начали реализовывать Vector API на других архитектурах, например RISC-V.

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

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

Мотивация

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

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

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

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

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

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

Чтобы уменьшить поверхность 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 и в AVX-512 на x64.

На таких платформах экземпляр 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), форма которого оптимальна для текущего CPU. Мы сохраняем его в поле static final, чтобы компилятор времени выполнения считал это значение константой и потому мог лучше оптимизировать векторное вычисление. Затем основной цикл проходит по входным массивам с шагом, равным длине вектора, то есть длине вида. Он загружает векторы float заданного вида из массивов a и b по соответствующему индексу, последовательно выполняет арифметические операции и сохраняет результат в массив c. Если после последней итерации остались элементы массива, результаты для этих хвостовых элементов вычисляются обычным скалярным циклом.

Эта реализация достигает оптимальной производительности на больших массивах. Компилятор HotSpot C2 генерирует на процессоре 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.

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

Intrinsic-функции для трансцендентных и тригонометрических операций

Vector API поддерживает поэлементные трансцендентные и тригонометрические операции над векторами с плавающей точкой. На x64 мы используем Intel Short Vector Math Library (SVML), а на ARM и RISC-V — SIMD Library for Evaluating Elementary Functions (SLEEF), чтобы предоставить оптимизированные intrinsic-реализации таких операций. Intrinsic-операции обладают теми же численными свойствами, что и соответствующие скалярные операции, определённые в java.lang.Math.

На всех платформах мы связываемся с нативными математическими функциями с помощью Foreign Function & Memory (FFM) API проекта Panama, что упрощает сопровождение и значительно сокращает объём кода на C++, необходимого в HotSpot для связывания нативных функций и их предоставления в виде заглушек (stubs), доступных C2. Вызов нативных функций по-прежнему требует некоторой специальной intrinsic-поддержки, но если проект Panama расширит поддержку нативных соглашений о вызовах до поддержки векторных значений, мы, возможно, сможем полностью от неё избавиться.

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

Реализация на основе SVML поддерживает x64 в Linux и Windows. Реализация на основе SLEEF поддерживает ARM и RISC-V в Linux и macOS.

Класс Float16

Vector API включает класс, основанный на значениях, Float16, который представляет 16-битные числа с плавающей точкой в формате IEEE 754 binary16. Компилятор HotSpot C2 может автоматически векторизовать сложение, вычитание, деление, умножение, извлечение квадратного корня и слитное умножение-сложение (fused multiply/add) на процессорах, которые это поддерживают.

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

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

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

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

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

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

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

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

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

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

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

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