Highway 是一个提供可移植 SIMD/矢量内部函数的 C++ 库。
以前在 Apache 2 下获得许可,现在作为 Apache 2 / BSD-3 获得双重许可。
我们对高性能软件充满热情。我们看到主要的未开发 CPU(服务器、移动设备、台式机)的潜力。高速公路适合想要 可靠且经济地突破以下领域的界限 软件。
CPU 提供 SIMD/矢量指令,将相同的操作应用于多个 数据项。这可以减少能源使用,例如减少五倍,因为更少的能源消耗 指令被执行。我们也经常看到 5-10 倍的加速。
根据这些,Highway 使 SIMD/矢量编程变得实用和可行 指导原则:
符合你的期望:Highway 是一个精心挑选的 C++ 库 无需大量编译器即可很好地映射到 CPU 指令的函数 转换。生成的代码对代码更具可预测性和鲁棒性 更改/编译器更新而不是自动矢量化。
适用于广泛使用的平台:Highway 支持五种架构;这 相同的应用程序代码可以针对各种指令集,包括 “可缩放”向量(编译时大小未知)。高速公路只需要 C++11 并支持四个编译器系列。如果你想在 Highway 上使用 其他平台,请提出问题。
灵活部署:使用 Highway 的应用程序可以在异构上运行 云或客户端设备,在 运行。或者,开发人员可以选择以单个指令集为目标 没有任何运行时开销。在这两种情况下,应用程序代码是相同的 除了与加一交换 代码行。
HWY_STATIC_DISPATCH
HWY_DYNAMIC_DISPATCH
适用于各种领域:Highway 提供了广泛的 操作, 用于图像处理(浮点)、压缩、视频 分析、线性代数、密码学、排序和随机生成。我们 认识到新的用例可能需要额外的运维,并乐于添加 它们在有意义的地方(例如,在某些架构上没有性能悬崖)。如果 你想讨论,请提出问题。
奖励数据并行设计:Highway 提供 Gather、 MaskedLoad 和 FixedTag 可加速传统数据结构。然而 最大的收益是通过设计算法和数据结构来解锁的 可扩展的向量。有用的技术包括批处理、阵列结构 布局和对齐/填充分配。
使用 Compiler Explorer 的在线演示:
我们观察到 Highway 在以下开源项目中被引用, 通过 sourcegraph.com 找到。大多数是 Github 存储库。如果你愿意 添加你的项目或直接链接到它,请随时提出问题或联系 我们通过以下电子邮件。
其他
如果你想获得 Highway,除了从此 Github 存储库克隆之外 或者把它作为一个 Git 子模块,你也可以在以下包中找到它 管理器或存储库:alpinelinux、conan-io、conda-forge、DragonFlyBSD、 freebsd、ghostbsd、microsoft/vcpkg、MidnightBSD、MSYS2、NetBSD、openSUSE、 opnsense,赛灵思/Vitis_Libraries。另请参阅 https://repology.org/project/highway-simd-library/versions 中的列表。
Highway 支持 22 个目标,按平台字母顺序列出:
EMU128
SCALAR;
NEON
SVE
SVE2
SVE_256
SVE2_128;
Z14
Z15;
PPC8
PPC9
PPC10
RVV
WASM
WASM_EMU256
HWY_WANT_WASM2
SSE2
SSSE3(~英特尔酷睿)
SSE4(~Nehalem,还包括 AES + CLMUL)。
AVX2(~Haswell,还包括 BMI2 + F16 + FMA)
AVX3(~天湖,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 trunk 编译并测试 使用我们的 QEMU 版本而没有额外的标志,那么它就有资格被收录 在我们的持续测试基础设施中。否则,目标将手动 在发布之前,使用 Clang 和 GCC 的选定版本/配置进行了测试。
SVE最初是使用farm_sve测试的(请参阅致谢)。
高速公路发布旨在遵循 semver.org 系统(MAJOR.次要。补丁), 在向后兼容添加后递增 MINOR,在向后兼容后递增 PATCH 向后兼容的修复。建议使用版本(而不是 Git 提示) 因为它们经过了更广泛的测试,请参见下文。
当前的 1.0 版本标志着对向后兼容性的日益关注。 使用记录功能的应用程序将与未来保持兼容 具有相同主版本号的更新。
使用最新版本的 Clang 构建持续集成测试(在 原生 x86,或适用于 RISC-V 和 Arm 的 QEMU)和 MSVC 2019(v19.28,在本机上运行) x86)。
在发布之前,我们还使用 Clang 和 GCC 在 x86 上进行测试,并通过 GCC 在 Armv7/8 上进行测试 交叉编译。请参阅测试过程 详。
该目录包含与 SIMD 相关的实用程序:一个具有 对齐行,一个数学库(已经实现了 16 个函数,主要是 三角函数),以及用于计算点积和排序的函数。
contrib
如果你只需要 x86 支持,你还可以使用 Agner Fog 的 VCL 向量类库。它包括许多 函数,包括一个完整的数学库。
如果你有使用 x86/NEON 内部函数的现有代码,你可能会对模拟这些内部函数的 SIMDe 感兴趣 使用其他平台的内部函数或自动矢量化的内部函数。
此项目使用 CMake 进行生成和生成。在基于 Debian 的系统中,你可以 通过以下方式安装它:
sudo apt install cmake
Highway 的单元测试使用 googletest。 默认情况下,Highway 的 CMake 在配置时下载此依赖项。 你可以通过将 CMake 变量设置为 ON 和 单独安装 gtest:
HWY_SYSTEM_GTEST
sudo apt install libgtest-dev
运行交叉编译测试需要操作系统的支持,在 Debian 上是 由软件包提供。
qemu-user-binfmt
要将 Highway 构建为共享或静态库(取决于BUILD_SHARED_LIBS), 可以使用标准的 CMake 工作流:
mkdir -p build && cd build
cmake ..
make -j && make test
或者你可以运行(在 Windows 上)。
run_tests.sh
run_tests.bat
Bazel 也支持构建,但它没有被广泛使用/测试。
在为 Armv7 构建时,当前编译器的限制要求你添加到 CMake 命令行;请参阅 #834 和 #1032。我们 了解正在开展消除此限制的工作。
-DHWY_CMAKE_ARM7:BOOL=ON
官方不支持在 32 位 x86 上构建,并且 AVX2/3 被禁用 默认在那里。请注意,johnplatts已经成功建造并运营了高速公路 在 GCC 7/8 和 Clang 8/11/12 上的 32 位 x86(包括 AVX2/3)上进行测试。在 Ubuntu 上 22.04,Clang 11 和 12,但不是更高版本,需要额外的编译器标志。Clang 10 及更早版本需要 以上加 .看 #1279.
-m32 -isystem /usr/i686-linux-gnu/include
-isystem /usr/i686-linux-gnu/include/c++/12/i686-linux-gnu
Highway 现在在 VCPKG 中可用
vcpkg install highway
vcpkg 中的高速公路端口由 Microsoft 团队成员和社区参与者保持最新状态。如果版本已过期,请在 vcpkg 存储库上创建问题或拉取请求。
你可以使用内部示例/作为起点。
benchmark
快速参考页面简要列出了所有操作 及其参数,instruction_matrix表示每个操作的指令数。
FAQ 回答了有关可移植性、API 设计和 在哪里可以找到更多信息。
我们建议尽可能使用完整的 SIMD 向量,以获得最佳性能 可移植性。若要获取它们,请将(或等效的)标记传递给 等函数,例如 。有两个 需要车道上限的用例的替代方案:
ScalableTag<float>
HWY_FULL(float)
Zero/Set/Load
对于最多车道,请指定或等效的 .实际车道数将四舍五入为 最接近的 2 的幂,例如 4 如果为 5,则 8 如果为 8。这是 对于数据结构(如窄矩阵)很有用。仍然需要循环 因为向量实际上可能比车道少。
N
CappedTag<T, N>
HWY_CAPPED(T, N)
N
N
N
N
对于两个通道的确切幂,请指定 。最大的 支持取决于目标,但保证至少是 .
N
FixedTag<T, N>
N
16/sizeof(T)
由于 ADL 限制,调用 Highway 操作的用户代码必须:
namespace hwy { namespace HWY_NAMESPACE {
namespace hn = hwy::HWY_NAMESPACE; hn::Add()
using hwy::HWY_NAMESPACE::Add;
此外,调用 Highway 操作的每个函数(例如 )必须 以 前缀 ,或位于 和 之间。Lambda 函数当前需要 before 他们的开场梅开二度。
Load
HWY_ATTR
HWY_BEFORE_NAMESPACE()
HWY_AFTER_NAMESPACE()
HWY_ATTR
使用 Highway 进入代码的入口点略有不同,具体取决于 它们使用静态或动态调度。在这两种情况下,我们建议 顶级函数接收一个或多个指向数组的指针,而不是 特定于目标的向量类型。
对于静态调度,将是 中最好的可用目标,即那些允许编译器使用的目标(参见快速参考)。里面的函数可以使用里面调用 它们在其中定义的同一模块。你可以从其他 模块,将其包装在常规函数中并声明常规 标头中的函数。
HWY_TARGET
HWY_BASELINE_TARGETS
HWY_NAMESPACE
HWY_STATIC_DISPATCH(func)(args)
对于动态调度,通过 to 使用的宏生成函数指针表 调用当前 CPU 支持目标的最佳函数指针。一个 模块会自动编译(参见快速参考)中的每个目标,如果是 定义并包含在内。请注意,第一次调用 的 ,或对第一个返回的指针的每次调用 调用 会涉及一些 CPU 检测开销。 你可以通过在调用 : 之前调用以下命令来防止这种情况发生。
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());
使用动态调度时,包含在翻译中 单位(.cc 文件),而不是标头。包含矢量代码的标头 一些翻译单元需要特殊的保护罩,例如 以下摘自:
foreach_target.h
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,我们建议编译为允许非内联函数 在寄存器中传递向量参数。如果打算同时使用 AVX2 目标 对于半角向量(例如 for),编译也很重要 跟。这似乎是生成 VEX 编码的 SSE4 的唯一方法 有关 MSVC 的说明。否则,将 VEX 编码的 AVX2 指令和 非 VEX SSE4 可能会导致严重的性能下降。不幸的是, 然后,生成的二进制文件将需要 AVX2。请注意,不需要这样的标志 clang 和 GCC,因为它们支持特定于目标的属性,我们用它们来 确保为 AVX2 目标正确生成 VEX 代码。
/Gv
PromoteTo
/arch:AVX2
在矢量化循环时,一个重要的问题是是否以及如何处理 多次迭代(“行程计数”,表示为 ),不均匀 除以向量大小。例如,可能需要避免 写入数组的末尾。
count
N = Lanes(d)
在本节中,让我们表示元素类型和 。 假设循环体作为函数给出。
T
d = ScalableTag<T>
template<bool partial, class D> void LoopBody(D d, size_t index, size_t max_n)
“剥离挖掘”是一种通过将循环转换为 外循环和内循环,使得内循环中的迭代次数 与矢量宽度匹配。然后,将内部循环替换为 vector 操作。
Highway 提供了几种循环矢量化策略:
确保所有输入/输出都已填充。那么(外部)循环就是
for (size_t i = 0; i < count; i += N) LoopBody<false>(d, i, 0);
这里不需要模板参数和第二个函数参数。
这是首选选项,除非是千和向量 操作流水线具有较长的延迟。情况就是这样 90 年代的超级计算机,但现在 ALU 很便宜,我们看到的大多数 实现将向量拆分为 1、2 或 4 个部分,因此成本很小 处理整个向量,即使我们不需要它们的所有通道。事实上 这避免了预测或部分预测的(可能很大)成本 在较旧的目标上加载/存储,并且不重复代码。
N
处理整个向量并包括以前处理的元素 在最后一个向量中:
for (size_t i = 0; i < count; i += N) LoopBody<false>(d, HWY_MIN(i, count - N), 0);
这是第二个首选选项,前提是并且是幂等的。某些元素可能会被处理两次,但 单个代码路径和完全矢量化通常是值得的。即使 ,通常将输入/输出填充到 是有意义的。
count >= N
LoopBody
count < N
N
使用 hwy/contrib/algo/transform-inl.h 中的函数。这 负责循环和余数处理,你只需定义一个 接收当前向量的通用 lambda 函数 (C++14) 或函子 从输入/输出数组,以及最多两个额外向量的可选向量 input 数组,并返回要写入输入/输出数组的值。
Transform*
下面是实现 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<false>(d, i, 0); for (; i < count; ++i) LoopBody<false>(CappedTag<T, 1>(), i, 0);
同样不需要模板参数和第二个函数参数。
这样可以避免重复代码,如果代码很大,则是合理的。 如果很小,则第二个循环可能比下一个选项慢。
count
count
如上所述处理整个向量,然后对带有掩码的修改进行单次调用:
LoopBody
size_t i = 0; for (; i + N <= count; i += N) { LoopBody<false>(d, i, 0); } if (i < count) { LoopBody<true>(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
我们使用了 Berenger 的 farm-sve 布拉马斯;事实证明,它对于检查 x86 开发中的 SVE 端口很有用 机器。
这不是官方支持的 Google 产品。 联系人:janwas@google.com