Highway 是一个 C++ 库,提供可移植的 SIMD/向量内在函数。
文档
以前在 Apache 2 下获得许可,现在在 Apache 2 / BSD-3 下获得双重许可。
我们对高性能软件充满热情。我们看到 CPU(服务器、移动设备、台式机)有巨大的未开发潜力。 Highway 适合那些想要可靠且经济地突破软件可能性界限的工程师。
CPU 提供 SIMD/向量指令,可将相同的操作应用于多个数据项。这可以减少能源使用,例如五倍,因为执行的指令更少。我们还经常看到5-10 倍的加速。
Highway 根据以下指导原则使 SIMD/向量编程变得实用和可行:
满足您的期望:Highway 是一个 C++ 库,具有精心选择的函数,可以很好地映射到 CPU 指令,而无需进行大量的编译器转换。与自动向量化相比,生成的代码对于代码更改/编译器更新更具可预测性和鲁棒性。
适用于广泛使用的平台:Highway支持五种架构;相同的应用程序代码可以针对各种指令集,包括具有“可扩展”向量的指令集(编译时大小未知)。 Highway 仅需要 C++11,并支持四个系列的编译器。如果您想在其他平台上使用 Highway,请提出问题。
部署灵活:使用 Highway 的应用程序可以在异构云或客户端设备上运行,在运行时选择最佳的可用指令集。或者,开发人员可以选择以单个指令集为目标,而无需任何运行时开销。在这两种情况下,除了将HWY_STATIC_DISPATCH
与HWY_DYNAMIC_DISPATCH
交换加上一行代码之外,应用程序代码是相同的。另请参阅@kfjahnke 对调度的介绍。
适用于各种领域:Highway提供了广泛的操作集,用于图像处理(浮点)、压缩、视频分析、线性代数、密码学、排序和随机生成。我们认识到新的用例可能需要额外的操作,并且很乐意在有意义的地方添加它们(例如,在某些架构上没有性能悬崖)。如果您想讨论,请提出问题。
奖励数据并行设计:Highway 提供了 Gather、MaskedLoad 和 FixTag 等工具来加速遗留数据结构。然而,最大的收益是通过为可扩展向量设计算法和数据结构来释放的。有用的技术包括批处理、数组结构布局和对齐/填充分配。
我们推荐以下资源作为入门资源:
使用编译器资源管理器的在线演示:
我们观察到,通过 sourcegraph.com 找到的以下开源项目中引用了 Highway。大多数是 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
( wasm128 的 2x 展开版本,如果定义了HWY_WANT_WASM2
则启用。这将继续受支持,直到它可能被未来版本的 WASM 取代。);SSE2
SSSE3
(~英特尔酷睿)SSE4
(~Nehalem,还包括 AES + CLMUL)。AVX2
(~Haswell,还包括 BMI2 + 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
(~蓝宝石急流,包括 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 上运行,或用于 RISC-V 和 Arm 的 QEMU)和 MSVC 2019(v19.28,在本机 x86 上运行)构建。
在发布之前,我们还使用 Clang 和 GCC 在 x86 上进行测试,并通过 GCC 交叉编译在 Armv7/8 上进行测试。详细信息请参见测试过程。
contrib
目录包含与 SIMD 相关的实用程序:具有对齐行的图像类、数学库(已实现 16 个函数,主要是三角函数)以及用于计算点积和排序的函数。
如果您只需要 x86 支持,您还可以使用 Agner Fog 的 VCL 矢量类库。它包含许多功能,包括完整的数学库。
如果您有使用 x86/NEON 内部函数的现有代码,您可能会对 SIMDe 感兴趣,它使用其他平台的内部函数或自动向量化来模拟这些内部函数。
该项目使用CMake来生成和构建。在基于 Debian 的系统中,您可以通过以下方式安装它:
sudo apt install cmake
Highway 的单元测试使用 googletest。默认情况下,Highway 的 CMake 在配置时下载此依赖项。您可以通过将HWY_SYSTEM_GTEST
CMake 变量设置为 ON 并单独安装 gtest 来避免这种情况:
sudo apt install libgtest-dev
或者,您可以定义HWY_TEST_STANDALONE=1
并删除每个 BUILD 文件中所有出现的gtest_main
,然后测试避免对 GUnit 的依赖。
运行交叉编译测试需要操作系统的支持,在 Debian 上由qemu-user-binfmt
软件包提供。
要将 Highway 构建为共享库或静态库(取决于 BUILD_SHARED_LIBS),可以使用标准 CMake 工作流程:
mkdir -p build && cd build
cmake ..
make -j && make test
或者您可以运行run_tests.sh
(在 Windows 上为run_tests.bat
)。
Bazel 也支持构建,但它的使用/测试并不广泛。
在针对 Armv7 进行构建时,当前编译器的限制要求您将-DHWY_CMAKE_ARM7:BOOL=ON
添加到 CMake 命令行;参见#834 和#1032。据我们了解,消除这一限制的工作正在进行中。
官方不支持在 32 位 x86 上构建,并且默认情况下禁用 AVX2/3。请注意,johnplatts 已在 32 位 x86(包括 AVX2/3)、GCC 7/8 和 Clang 8/11/12 上成功构建并运行了 Highway 测试。在 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
作为起点。
快速参考页面简要列出了所有操作及其参数,指令矩阵表示每个操作的指令数。
常见问题解答回答了有关可移植性、API 设计以及在哪里可以找到更多信息的问题。
我们建议尽可能使用完整的 SIMD 向量,以获得最大的性能可移植性。要获取它们,请将ScalableTag
(或等效的HWY_FULL(float)
)标记传递给Zero/Set/Load
等函数。对于需要通道上限的用例,有两种替代方案:
对于最多N
通道,请指定CappedTag
或等效的HWY_CAPPED(T, N)
。实际通道数将N
向下舍入到最接近的 2 的幂,例如,如果N
为 5,则为 4;如果N
为 8,则为 8。这对于窄矩阵等数据结构很有用。仍然需要循环,因为向量实际上可能少于N
通道。
对于两个N
通道的幂,请指定FixedTag
。支持的最大N
取决于目标,但保证至少为16/sizeof(T)
。
由于 ADL 限制,调用 Highway ops 的用户代码必须:
namespace hwy { namespace HWY_NAMESPACE {
;或者namespace hn = hwy::HWY_NAMESPACE; hn::Add()
;或者using hwy::HWY_NAMESPACE::Add;
。此外,每个调用 Highway ops 的函数(例如Load
)必须以HWY_ATTR
为前缀,或者位于HWY_BEFORE_NAMESPACE()
和HWY_AFTER_NAMESPACE()
之间。 Lambda 函数当前在左大括号之前需要HWY_ATTR
。
不要对 SIMD 向量使用命名空间范围或static
初始化程序,因为这可能会在使用运行时分派时导致 SIGILL,并且编译器会选择为当前 CPU 不支持的目标编译的初始化程序。相反,通过Set
初始化的常量通常应该是局部(const)变量。
使用 Highway 的代码入口点根据使用静态调度还是动态调度而略有不同。在这两种情况下,我们建议顶级函数接收一个或多个指向数组的指针,而不是特定于目标的向量类型。
对于静态调度, HWY_TARGET
将是HWY_BASELINE_TARGETS
中最好的可用目标,即那些允许编译器使用的目标(请参阅快速参考)。 HWY_NAMESPACE
中的函数可以在定义它们的同一模块中使用HWY_STATIC_DISPATCH(func)(args)
进行调用。您可以通过将其包装在常规函数中并在标头中声明常规函数来从其他模块调用该函数。
对于动态调度,通过HWY_EXPORT
宏生成函数指针表, HWY_DYNAMIC_DISPATCH(func)(args)
使用该宏为当前 CPU 支持的目标调用最佳函数指针。如果定义了HWY_TARGET_INCLUDE
并且包含了foreach_target.h
,则会自动为HWY_TARGETS
中的每个目标编译一个模块(请参阅快速参考)。请注意,第一次调用HWY_DYNAMIC_DISPATCH
或每次调用第一次调用HWY_DYNAMIC_POINTER
返回的指针都会涉及一些 CPU 检测开销。您可以通过在调用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
进行编译也很重要。这似乎是在 MSVC 上可靠生成 VEX 编码的 SSE 指令的唯一方法。有时,如果 MSVC 与 AVX 混合,MSVC 会生成 VEX 编码的 SSE 指令,但并非总是如此,请参阅 DevCom-10618264。否则,混合 VEX 编码的 AVX2 指令和非 VEX SSE 可能会导致严重的性能下降。不幸的是,使用/arch:AVX2
选项,生成的二进制文件将需要 AVX2。请注意,clang 和 GCC 不需要这样的标志,因为它们支持特定于目标的属性,我们使用这些属性来确保为 AVX2 目标生成正确的 VEX 代码。
对循环进行向量化时,一个重要的问题是是否以及如何处理未均匀划分向量大小N = Lanes(d)
的多次迭代(“行程计数”,表示为count
)。例如,可能需要避免写入超过数组末尾。
在本节中,让T
表示元素类型,并且d = ScalableTag
。假设循环体作为函数template
给出。
“条带挖掘”是一种通过将循环转换为外循环和内循环来向量化循环的技术,使得内循环中的迭代次数与向量宽度匹配。然后,将内循环替换为向量运算。
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
通常也是有意义的。
使用 hwy/contrib/algo/transform-inl.h 中的Transform*
函数。这负责循环和余数处理,您只需定义一个通用 lambda 函数 (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
内部使用模板参数和第三个函数参数,以非原子方式“混合” v
的前num_remaining
通道与后续位置的内存先前内容: BlendedStore(v, FirstN(d, num_remaining), d, pointer);
。类似地, MaskedLoad(FirstN(d, num_remaining), d, pointer)
加载前num_remaining
元素并在其他通道中返回零。
当无法确保填充向量时,这是一个很好的默认值,但只有#if !HWY_MEM_OPS_MIGHT_FAULT
才是安全的!与标量循环相比,只需要一次最终迭代。两个循环体增加的代码大小预计是值得的,因为它避免了除最终迭代之外的所有迭代中的屏蔽成本。
我们使用了Berenger Bramas的farm-sve;事实证明,它对于检查 x86 开发机器上的 SVE 端口很有用。
这不是 Google 官方支持的产品。联系方式:[email protected]