Highway — это библиотека C++, предоставляющая переносимые встроенные функции SIMD/векторов.
Документация
Ранее лицензировался под Apache 2, теперь имеет двойную лицензию как Apache 2/BSD-3.
Мы увлечены высокопроизводительным программным обеспечением. Мы видим большой неиспользованный потенциал в процессорах (серверах, мобильных устройствах, настольных компьютерах). Highway предназначен для инженеров, которые хотят надежно и экономично расширить границы возможного в программном обеспечении.
ЦП предоставляют SIMD/векторные инструкции, которые применяют одну и ту же операцию к нескольким элементам данных. Это может сократить потребление энергии, например, в пять раз , поскольку выполняется меньше команд. Мы также часто видим ускорение в 5-10 раз .
Highway делает SIMD/векторное программирование практичным и осуществимым в соответствии со следующими руководящими принципами:
Делает то, что вы ожидаете : Highway — это библиотека C++ с тщательно подобранными функциями, которые хорошо сопоставляются с инструкциями ЦП без обширных преобразований компилятора. Полученный код более предсказуем и устойчив к изменениям кода/обновлениям компилятора, чем автовекторизация.
Работает на широко используемых платформах : Highway поддерживает пять архитектур; один и тот же код приложения может работать с различными наборами инструкций, в том числе с «масштабируемыми» векторами (размер неизвестен во время компиляции). Highway требует только C++11 и поддерживает четыре семейства компиляторов. Если вы хотите использовать Highway на других платформах, поднимите вопрос.
Гибкость в развертывании . Приложения, использующие Highway, могут работать в гетерогенных облаках или на клиентских устройствах, выбирая лучший доступный набор инструкций во время выполнения. В качестве альтернативы разработчики могут выбрать один набор инструкций без каких-либо накладных расходов во время выполнения. В обоих случаях код приложения один и тот же, за исключением замены HWY_STATIC_DISPATCH
на HWY_DYNAMIC_DISPATCH
плюс одна строка кода. См. также введение @kfjahnke в диспетчеризацию.
Подходит для различных областей : Highway предоставляет обширный набор операций, используемых для обработки изображений (с плавающей запятой), сжатия, анализа видео, линейной алгебры, криптографии, сортировки и генерации случайных чисел. Мы понимаем, что новые сценарии использования могут потребовать дополнительных операций, и рады добавлять их там, где это имеет смысл (например, отсутствие скачков производительности на некоторых архитектурах). Если вы хотите обсудить, пожалуйста, сообщите о проблеме.
Вознаграждает параллельную разработку данных . Highway предоставляет такие инструменты, как Gather, MaskedLoad и FixTag, позволяющие ускорить работу устаревших структур данных. Однако наибольшие выгоды можно получить за счет разработки алгоритмов и структур данных для масштабируемых векторов. Полезные методы включают пакетную обработку, макеты структуры массива и выравнивание/дополнение.
Мы рекомендуем эти ресурсы для начала:
Онлайн-демонстрации с использованием Compiler Explorer:
Мы заметили, что Highway упоминается в следующих проектах с открытым исходным кодом, найденных на sourcegraph.com. Большинство из них — репозитории GitHub. Если вы хотите добавить свой проект или ссылку на него напрямую, не стесняйтесь поднять вопрос или свяжитесь с нами по электронной почте, указанной ниже.
Другой
Если вы хотите получить Highway, помимо клонирования из этого репозитория GitHub или использования его в качестве подмодуля Git, вы также можете найти его в следующих менеджерах пакетов или репозиториях:
См. также список по адресу https://repology.org/project/highway-simd-library/versions.
Highway поддерживает 24 цели, перечисленные в алфавитном порядке платформ:
EMU128
, SCALAR
;NEON_WITHOUT_AES
, NEON
, NEON_BF16
, SVE
, SVE2
, SVE_256
, SVE2_128
;Z14
, Z15
;PPC8
(v2.07), PPC9
(v3.0), PPC10
(v3.1B, пока не поддерживается из-за ошибок компилятора, см. #1207; также требуется QEMU 7.2);RVV
(1,0);WASM
, WASM_EMU256
(2-кратная развернутая версия wasm128, включенная, если определен HWY_WANT_WASM2
. Она будет поддерживаться до тех пор, пока не будет потенциально заменена будущей версией WASM.);SSE2
SSSE3
(~Intel Core)SSE4
(~Nehalem, также включает AES + CLMUL).AVX2
(~Haswell, также включает ИМТ2 + F16 + FMA)AVX3
(~Skylake, AVX-512F/BW/CD/DQ/VL)AVX3_DL
(~Icelake, включает BitAlg + CLMUL + GFNI + VAES + VBMI + VBMI2 + VNNI + VPOPCNT; требуется согласие путем определения HWY_WANT_AVX3_DL
, если не выполняется компиляция для статической отправки),AVX3_ZEN4
(аналогично AVX3_DL, но оптимизирован для AMD Zen4; требуется согласие путем определения HWY_WANT_AVX3_ZEN4
при компиляции для статической отправки, но включено по умолчанию для отправки во время выполнения),AVX3_SPR
(~Sapphire Rapids, включает AVX-512FP16)Наша политика заключается в том, что, если не указано иное, целевые объекты будут поддерживаться до тех пор, пока их можно (кросс-)компилировать с помощью поддерживаемых в настоящее время Clang или GCC и протестировать с использованием QEMU. Если цель можно скомпилировать с помощью магистрали LLVM и протестировать с использованием нашей версии QEMU без дополнительных флагов, то она подходит для включения в нашу инфраструктуру непрерывного тестирования. В противном случае цель будет проверена вручную перед выпуском с выбранными версиями/конфигурациями Clang и GCC.
Первоначально SVE тестировался с использованием Farm_sve (см. Благодарности).
Релизы Highway следуют системе semver.org (MAJOR.MINOR.PATCH), увеличивая MINOR после дополнений с обратной совместимостью и PATCH после исправлений обратной совместимости. Мы рекомендуем использовать релизы (а не советы по Git), поскольку они тестируются более тщательно, см. ниже.
Текущая версия 1.0 свидетельствует о повышенном внимании к обратной совместимости. Приложения, использующие документированные функции, останутся совместимыми с будущими обновлениями, имеющими тот же основной номер версии.
Тесты непрерывной интеграции построены с использованием последней версии Clang (работающей на собственном x86 или QEMU для RISC-V и Arm) и MSVC 2019 (v19.28, работающего на собственном x86).
Перед выпуском мы также тестируем на x86 с помощью Clang и GCC, а также на Armv7/8 с помощью кросс-компиляции GCC. Подробности смотрите в процессе тестирования.
Каталог contrib
содержит утилиты, связанные с SIMD: класс изображения с выровненными строками, математическую библиотеку (уже реализовано 16 функций, в основном тригонометрия) и функции для вычисления скалярных произведений и сортировки.
Если вам требуется только поддержка x86, вы также можете использовать библиотеку векторных классов VCL Agner Fog. Он включает в себя множество функций, включая полную математическую библиотеку.
Если у вас есть код, использующий встроенные функции x86/NEON, вас может заинтересовать SIMDe, который эмулирует эти встроенные функции с использованием встроенных функций других платформ или автовекторизации.
В этом проекте для создания и сборки используется CMake. В системе на базе Debian вы можете установить его через:
sudo apt install cmake
Модульные тесты Highway используют Googletest. По умолчанию CMake Highway загружает эту зависимость во время настройки. Этого можно избежать, установив для переменной CMake HWY_SYSTEM_GTEST
значение ON и установив gtest отдельно:
sudo apt install libgtest-dev
В качестве альтернативы вы можете определить HWY_TEST_STANDALONE=1
и удалить все вхождения gtest_main
в каждом файле BUILD, тогда тесты избегают зависимости от GUnit.
Для запуска кросс-компилированных тестов требуется поддержка ОС, которая в Debian обеспечивается пакетом qemu-user-binfmt
.
Чтобы собрать Highway как общую или статическую библиотеку (в зависимости от BUILD_SHARED_LIBS), можно использовать стандартный рабочий процесс CMake:
mkdir -p build && cd build
cmake ..
make -j && make test
Или вы можете запустить run_tests.sh
( run_tests.bat
в Windows).
Bazel также поддерживается для сборки, но он не так широко используется и не тестируется.
При сборке для Armv7 ограничение текущих компиляторов требует добавления -DHWY_CMAKE_ARM7:BOOL=ON
в командную строку CMake; см. №834 и №1032. Мы понимаем, что ведется работа по снятию этого ограничения.
Сборка на 32-битной версии x86 официально не поддерживается, и AVX2/3 там по умолчанию отключены. Обратите внимание, что johnplatts успешно собрал и запустил тесты Highway на 32-разрядной версии x86, включая AVX2/3, на GCC 7/8 и Clang 8/11/12. В Ubuntu 22.04, Clang 11 и 12, но не в более поздних версиях, требуются дополнительные флаги компилятора -m32 -isystem /usr/i686-linux-gnu/include
. Clang 10 и более ранние версии требуют вышеуказанного плюс -isystem /usr/i686-linux-gnu/include/c++/12/i686-linux-gnu
. См. № 1279.
шоссе теперь доступно в vcpkg
vcpkg install highway
Порт шоссе в vcpkg постоянно обновляется членами команды Microsoft и участниками сообщества. Если версия устарела, создайте проблему или запрос на извлечение в репозитории vcpkg.
Вы можете использовать benchmark
внутри примеров/ в качестве отправной точки.
На странице краткой справки кратко перечислены все операции и их параметры, а в поле Instruction_matrix указано количество инструкций на операцию.
Часто задаваемые вопросы отвечают на вопросы о переносимости, дизайне API и о том, где найти дополнительную информацию.
Мы рекомендуем по возможности использовать полные векторы SIMD для максимальной переносимости производительности. Чтобы получить их, передайте тег ScalableTag
(или, что эквивалентно, HWY_FULL(float)
) в такие функции, как Zero/Set/Load
. Существует две альтернативы для случаев использования, требующих верхней границы полос:
Для N
полос укажите CappedTag
или эквивалент HWY_CAPPED(T, N)
. Фактическое количество полос будет равно N
округленному до ближайшей степени двойки, например 4, если N
равно 5, или 8, если N
равно 8. Это полезно для структур данных, таких как узкая матрица. Цикл по-прежнему необходим, поскольку на самом деле векторы могут иметь менее N
полос.
Чтобы получить ровно степень двух N
полос, укажите FixedTag
. Максимальный поддерживаемый N
зависит от цели, но гарантированно будет не меньше 16/sizeof(T)
.
Из-за ограничений ADL пользовательский код, вызывающий операторы Highway, должен либо:
namespace hwy { namespace HWY_NAMESPACE {
; илиnamespace hn = hwy::HWY_NAMESPACE; hn::Add()
; илиusing hwy::HWY_NAMESPACE::Add;
. Кроме того, каждая функция, вызывающая операции Highway (например, Load
), должна иметь префикс HWY_ATTR
или находиться между HWY_BEFORE_NAMESPACE()
и HWY_AFTER_NAMESPACE()
. Лямбда-функциям в настоящее время требуется HWY_ATTR
перед открывающей скобкой.
Не используйте область пространства имен и static
инициализаторы для векторов SIMD, поскольку это может вызвать SIGILL при использовании диспетчеризации во время выполнения, и компилятор выбирает инициализатор, скомпилированный для цели, не поддерживаемой текущим ЦП. Вместо этого константы, инициализированные с помощью Set
, обычно должны быть локальными (const) переменными.
Точки входа в код с использованием Highway немного различаются в зависимости от того, используют ли они статическую или динамическую отправку. В обоих случаях мы рекомендуем, чтобы функция верхнего уровня получала один или несколько указателей на массивы, а не на целевые типы векторов.
Для статической отправки HWY_TARGET
будет лучшей доступной целью среди HWY_BASELINE_TARGETS
, т. е. тех, которые разрешены для использования компилятором (см. краткую справку). Функции внутри HWY_NAMESPACE
можно вызывать с помощью HWY_STATIC_DISPATCH(func)(args)
внутри того же модуля, в котором они определены. Вы можете вызвать функцию из других модулей, обернув ее в обычную функцию и объявив обычную функцию в заголовке.
Для динамической диспетчеризации таблица указателей функций генерируется с помощью макроса HWY_EXPORT
, который используется HWY_DYNAMIC_DISPATCH(func)(args)
для вызова лучшего указателя функции для поддерживаемых текущим ЦП целей. Модуль автоматически компилируется для каждой цели в HWY_TARGETS
(см. краткую справку), если определен HWY_TARGET_INCLUDE
и включен foreach_target.h
. Обратите внимание, что первый вызов HWY_DYNAMIC_DISPATCH
или каждый вызов указателя, возвращаемого первым вызовом HWY_DYNAMIC_POINTER
, включает в себя некоторые накладные расходы на обнаружение ЦП. Вы можете предотвратить это, вызвав перед любым вызовом HWY_DYNAMIC_*
следующую команду: hwy::GetChosenTarget().Update(hwy::SupportedTargets());
.
См. также отдельное введение в динамическую отправку от @kfjahnke.
При использовании динамической отправки foreach_target.h
включается из единиц перевода (файлов .cc), а не из заголовков. Заголовки, содержащие векторный код, общий для нескольких единиц трансляции, требуют специального включения, например, следующего, взятого из examples/skeleton-inl.h
:
#if defined(HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_) == defined(HWY_TARGET_TOGGLE)
#ifdef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#undef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#else
#define HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#endif
#include "hwy/highway.h"
// Your vector code
#endif
По соглашению мы называем такие заголовки -inl.h
, поскольку их содержимое (часто шаблоны функций) обычно встроено.
Приложения должны компилироваться с включенной оптимизацией. Без встраивания SIMD-код может замедлиться в 10–100 раз. Для clang и GCC обычно достаточно -O2
.
Для MSVC мы рекомендуем компилировать с параметром /Gv
, чтобы позволить невстроенным функциям передавать векторные аргументы в регистрах. Если вы собираетесь использовать цель AVX2 вместе с векторами половинной ширины (например, для PromoteTo
), также важно скомпилировать с /arch:AVX2
. Кажется, это единственный способ надежно генерировать инструкции SSE в кодировке VEX на MSVC. Иногда MSVC генерирует инструкции SSE в кодировке VEX, если они смешаны с AVX, но не всегда, см. DevCom-10618264. В противном случае смешивание инструкций AVX2, закодированных в VEX, и инструкций SSE, отличных от VEX, может привести к серьезному снижению производительности. К сожалению, при использовании параметра /arch:AVX2
для полученного двоичного файла потребуется AVX2. Обратите внимание, что для clang и GCC такой флаг не требуется, поскольку они поддерживают атрибуты, специфичные для цели, которые мы используем для обеспечения правильной генерации кода VEX для целей AVX2.
При векторизации цикла важным вопросом является то, следует ли и как бороться с количеством итераций («количество поездок», обозначается как count
), которые не делят поровну размер вектора N = Lanes(d)
. Например, может возникнуть необходимость избегать записи за конец массива.
В этом разделе пусть T
обозначает тип элемента, а d = ScalableTag
. Предположим, что тело цикла задано в виде функции template
.
«Strip-mining» — это метод векторизации цикла путем преобразования его во внешний цикл и внутренний цикл таким образом, чтобы количество итераций во внутреннем цикле соответствовало ширине вектора. Затем внутренний цикл заменяется векторными операциями.
Highway предлагает несколько стратегий векторизации цикла:
Убедитесь, что все входы/выходы заполнены. Тогда (внешний) цикл просто
for (size_t i = 0; i < count; i += N) LoopBody(d, i, 0);
Здесь параметр шаблона и второй аргумент функции не нужны.
Это предпочтительный вариант, если только N
не исчисляется тысячами и векторные операции не выполняются с большими задержками. Так было с суперкомпьютерами 90-х годов, но сегодня ALU дешевы, и мы видим, что большинство реализаций разделяют векторы на 1, 2 или 4 части, поэтому обработка целых векторов требует небольших затрат, даже если нам не нужны все их полосы. Действительно, это позволяет избежать (потенциально больших) затрат на предикацию или частичную загрузку/сохранение старых целей и не дублирует код.
Обрабатывать целые векторы и включать в последний вектор ранее обработанные элементы:
for (size_t i = 0; i < count; i += N) LoopBody(d, HWY_MIN(i, count - N), 0);
Это второй предпочтительный вариант при условии, что count >= N
и LoopBody
является идемпотентным. Некоторые элементы могут быть обработаны дважды, но обычно один путь кода и полная векторизация того стоят. Даже если count < N
, обычно имеет смысл дополнить входы/выходы до N
Используйте функции Transform*
в hwy/contrib/algo/transform-inl.h. Это обеспечивает обработку цикла и остатка, и вы просто определяете общую лямбда-функцию (C++14) или функтор, которая получает текущий вектор из массива ввода/вывода, а также, при необходимости, векторы из двух дополнительных входных массивов и возвращает значение для записи в массив ввода/вывода.
Вот пример реализации функции BLAS SAXPY ( alpha * x + y
):
Transform1(d, x, n, y, [](auto d, const auto v, const auto v1) HWY_ATTR {
return MulAdd(Set(d, alpha), v, v1);
});
Обработайте целые векторы, как указано выше, а затем выполните скалярный цикл:
size_t i = 0;
for (; i + N <= count; i += N) LoopBody(d, i, 0);
for (; i < count; ++i) LoopBody(CappedTag(), i, 0);
Параметр шаблона и аргументы второй функции снова не нужны.
Это позволяет избежать дублирования кода и является разумным, если count
велико. Если count
невелик, второй цикл может быть медленнее, чем следующий вариант.
Обработайте целые векторы, как указано выше, с последующим вызовом модифицированного LoopBody
с маскированием:
size_t i = 0;
for (; i + N <= count; i += N) {
LoopBody(d, i, 0);
}
if (i < count) {
LoopBody(d, i, count - i);
}
Теперь параметр шаблона и третий аргумент функции можно использовать внутри LoopBody
для неатомарного «смешивания» первых num_remaining
полос v
с предыдущим содержимым памяти в последующих местах: BlendedStore(v, FirstN(d, num_remaining), d, pointer);
. Аналогично, MaskedLoad(FirstN(d, num_remaining), d, pointer)
загружает первые элементы num_remaining
и возвращает ноль на других полосах.
Это хорошее значение по умолчанию, когда невозможно обеспечить заполнение векторов, но безопасно только #if !HWY_MEM_OPS_MIGHT_FAULT
! В отличие от скалярного цикла, требуется только одна финальная итерация. Ожидается, что увеличение размера кода за счет двух тел цикла будет целесообразным, поскольку позволяет избежать затрат на маскирование во всех итерациях, кроме последней.
Мы использовали фарм-све Беренгера Брамаса; он оказался полезным для проверки порта SVE на машине разработки x86.
Это не официально поддерживаемый продукт Google. Контакт: [email protected]