Project Icon

highway

多架构支持的高性能SIMD向量化库

Highway是一个跨平台C++向量化库,支持x86、ARM等多种CPU架构的SIMD指令。它提供统一API,实现一次编码多平台运行,支持静态编译和运行时动态调度。Highway适用于图像处理、数据压缩、密码学等领域,提供丰富的向量化操作,易用性和性能兼备,是开发高性能软件的理想选择。

高效且性能可移植的向量软件

Highway 是一个提供可移植 SIMD/向量内联函数的 C++ 库。

文档

之前采用 Apache 2 许可,现在双重许可为 Apache 2 / BSD-3。

为什么

我们热衷于高性能软件。我们看到 CPU(服务器、移动设备、桌面)中存在巨大的未开发潜力。Highway 适用于那些希望可靠且经济地突破软件可能性边界的工程师。

如何

CPU 提供 SIMD/向量指令,可以对多个数据项应用相同的操作。这可以将能耗降低约 5 倍,因为执行的指令更少。我们通常还能看到 5-10 倍的速度提升。

Highway 使 SIMD/向量编程变得实用可行,遵循以下指导原则:

符合预期:Highway 是一个 C++ 库,具有精心选择的函数,无需大量编译器转换即可很好地映射到 CPU 指令。与自动向量化相比,生成的代码对代码更改/编译器更新更具可预测性和鲁棒性。

适用于广泛使用的平台:Highway 支持五种架构;相同的应用程序代码可以针对各种指令集,包括那些具有"可扩展"向量(编译时大小未知)的指令集。Highway 仅需要 C++11,并支持四个编译器系列。如果您想在其他平台上使用 Highway,请提出问题。

部署灵活:使用 Highway 的应用程序可以在异构云或客户端设备上运行,在运行时选择最佳可用的指令集。或者,开发人员可以选择针对单一指令集,无任何运行时开销。在这两种情况下,应用程序代码都是相同的,只需将 HWY_STATIC_DISPATCH 替换为 HWY_DYNAMIC_DISPATCH 并添加一行代码。

适用于各种领域:Highway 提供了广泛的操作集,用于图像处理(浮点)、压缩、视频分析、线性代数、密码学、排序和随机生成。我们认识到新的用例可能需要额外的操作,并乐意在合理的情况下添加它们(例如,在某些架构上不会出现性能断崖)。如果您想讨论,请提出问题。

奖励数据并行设计:Highway 提供了 Gather、MaskedLoad 和 FixedTag 等工具,以实现对传统数据结构的加速。然而,最大的收益是通过为可扩展向量设计算法和数据结构来实现的。有用的技术包括批处理、数组结构布局和对齐/填充分配。

我们推荐以下资源作为入门:

示例

使用 Compiler Explorer 的在线演示:

我们观察到 Highway 在以下开源项目中被引用,这些项目通过 sourcegraph.com 找到。大多数是 GitHub 仓库。如果您想添加您的项目或直接链接到它,请随时提出问题或通过以下电子邮件与我们联系。

其他

  • C++ SIMD 库评估: "Highway 在多个 SIMD 扩展中表现出色 [..] 。因此,Highway 目前可能是许多软件项目最合适的 SIMD 库。"
  • zimt:用于处理 n 维数组的多线程 SIMD 代码的 C++11 模板库
  • 向量化快速排序论文

如果您想获取 Highway,除了从这个 GitHub 仓库克隆或将其用作 Git 子模块外,您还可以在以下包管理器或仓库中找到它:

  • alpinelinux
  • conan-io
  • conda-forge
  • DragonFlyBSD
  • fd00/yacp
  • freebsd
  • getsolus/packages
  • ghostbsd
  • microsoft/vcpkg
  • MidnightBSD
  • MSYS2
  • NetBSD
  • openSUSE
  • opnsense
  • Xilinx/Vitis_Libraries
  • xmake-io/xmake-repo

另请参阅 https://repology.org/project/highway-simd-library/versions 的列表。

当前状态

目标

Highway 支持 24 个目标,按平台字母顺序列出:

  • 任意:EMU128SCALAR
  • Armv7+:NEON_WITHOUT_AESNEONNEON_BF16SVESVE2SVE_256SVE2_128
  • IBM Z:Z14Z15
  • POWER:PPC8(v2.07)、PPC9(v3.0)、PPC10(v3.1B,由于编译器错误尚不支持, 参见 #1207;还需要 QEMU 7.2);
  • RISC-V:RVV(1.0);
  • WebAssembly:WASMWASM_EMU256(wasm128 的 2 倍展开版本, 如果定义了 HWY_WANT_WASM2 则启用。在可能被未来版本的 WASM 取代之前, 这将继续得到支持。);
  • x86:
    • SSE2
    • SSSE3(~Intel Core)
    • 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(~Sapphire Rapids,包括 AVX-512FP16)

我们的政策是,除非另有说明,只要可以使用当前支持的 Clang 或 GCC(交叉)编译,并使用 QEMU 进行测试,就会继续支持目标。如果目标可以使用 LLVM trunk 编译,并使用我们版本的 QEMU 进行测试而无需额外标志,那么它就有资格纳入我们的持续测试基础设施。否则,目标将在发布前使用选定版本/配置的 Clang 和 GCC 进行手动测试。

SVE 最初使用 farm_sve 进行测试(参见致谢)。

版本控制

Highway 发布旨在遵循 semver.org 系统(主版本号.次版本号.修订号),在向后兼容的添加后递增次版本号,在向后兼容的修复后递增修订号。我们建议使用发布版(而不是 Git 提示),因为它们经过更广泛的测试,详见下文。

当前版本 1.0 表示更加注重向后兼容性。使用文档化功能的应用程序将与具有相同主版本号的未来更新保持兼容。

测试

持续集成测试使用最新版本的 Clang(在原生 x86 上运行,或在 RISC-V 和 Arm 上使用 QEMU)和 MSVC 2019(v19.28,在原生 x86 上运行)进行构建。

在发布之前,我们还会在 x86 上使用 Clang 和 GCC 进行测试,并通过 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也支持构建,但使用和测试不如CMake广泛。

在为Armv7构建时,当前编译器的限制要求您在CMake命令行中添加-DHWY_CMAKE_ARM7:BOOL=ON;参见 #834 和 #1032。据了解,正在进行消除这一限制的工作。

不官方支持在32位x86上构建,默认情况下AVX2/3在那里被禁用。注意johnplatts已经成功地在32位x86上构建并运行了Highway测试,包括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构建highway

highway现在可以在vcpkg中使用

vcpkg install highway

vcpkg中的highway端口由Microsoft团队成员和社区贡献者保持最新。如果版本过时,请在vcpkg仓库上创建一个问题或拉取请求

快速开始

您可以使用examples/中的benchmark作为起点。

快速参考页面简要列出了所有操作及其参数,而指令矩阵指示了每个操作的指令数量。

FAQ回答了关于可移植性、API设计以及在哪里可以找到更多信息的问题。

我们建议尽可能使用完整的SIMD向量以获得最大的性能可移植性。要获得它们,请将ScalableTag<float>(或等效的HWY_FULL(float))标签传递给Zero/Set/Load等函数。对于需要对车道数量设置上限的用例,有两种替代方案:

  • 对于最多N个车道,指定CappedTag<T, N>或等效的HWY_CAPPED(T, N)。实际车道数将是N向下舍入到最接近的2的幂,例如如果N是5则为4,如果N是8则为8。这对于窄矩阵等数据结构很有用。仍然需要一个循环,因为向量实际上可能少于N个车道。

  • 对于恰好2的幂N个车道,指定FixedTag<T, N>。支持的最大N取决于目标,但保证至少为16/sizeof(T)

由于ADL限制,调用Highway操作的用户代码必须:

  • 位于namespace hwy { namespace HWY_NAMESPACE {内;或
  • 为每个操作添加前缀,如namespace hn = hwy::HWY_NAMESPACE; hn::Add();或
  • 为每个使用的操作添加using声明:using hwy::HWY_NAMESPACE::Add;

此外,调用Highway操作(如Load)的每个函数必须以HWY_ATTR为前缀,或者位于HWY_BEFORE_NAMESPACE()HWY_AFTER_NAMESPACE()之间。Lambda函数目前需要在开头大括号前加上HWY_ATTR

不要对SIMD向量使用命名空间作用域或static初始化器,因为在使用运行时调度时,如果编译器选择了为不受当前CPU支持的目标编译的初始化器,这可能导致SIGILL。相反,通过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());

使用动态调度时,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"
// 您的向量代码
#endif

按照惯例,我们将这些头文件命名为-inl.h,因为它们的内容(通常是函数模板)通常是内联的。

编译器标志

应用程序应该在启用优化的情况下编译。没有内联,SIMD代码可能会慢10到100倍。对于clang和GCC,-O2通常就足够了。

对于MSVC,我们建议使用/Gv编译,以允许非内联函数在寄存器中传递向量参数。如果打算将AVX2目标与半宽向量一起使用(例如用于PromoteTo),那么使用/arch:AVX2编译也很重要。这似乎是在MSVC上可靠生成VEX编码的SSE指令的唯一方法。有时MSVC会生成VEX编码的SSE指令,如果它们与AVX混合,但并非总是如此,参见DevCom-10618264。否则,混合VEX编码的AVX2指令和非VEX SSE可能会导致严重的性能下降。不幸的是,使用/arch:AVX2选项,生成的二进制文件将需要AVX2。请注意,clang和GCC不需要这样的标志,因为它们支持特定目标的属性,我们使用这些属性来确保为AVX2目标正确生成VEX代码。

条带化循环

在向量化循环时,一个重要的问题是如何处理不能被向量大小N = Lanes(d)整除的迭代次数('行程计数',表示为count)。例如,可能需要避免写入数组末尾之外。

在本节中,让T表示元素类型,d = ScalableTag<T>。假设循环体作为函数template<bool partial, class D> void LoopBody(D d, size_t index, size_t max_n)给出。

"条带化"是一种通过将循环转换为外循环和内循环来向量化循环的技术,使得内循环中的迭代次数与向量宽度匹配。然后,内循环被替换为向量操作。

Highway提供了几种循环向量化策略:

  • 确保所有输入/输出都经过填充。然后(外)循环简单地为
for (size_t i = 0; i < count; i += N) LoopBody<false>(d, i, 0);

在这里,模板参数和第二个函数参数是不需要的。 这是首选方案,除非 N 达到上千且向量操作存在长延迟的流水线处理。这种情况在90年代的超级计算机中存在,但现今ALU成本低廉,大多数实现将向量分为1、2或4部分,因此即使不需要所有通道,处理整个向量的成本也很小。事实上,这避免了在较旧目标上谓词或部分加载/存储的(可能较大的)成本,且不会重复代码。

  • 处理整个向量,并在最后一个向量中包含先前处理过的元素:

    for (size_t i = 0; i < count; i += N) LoopBody<false>(d, HWY_MIN(i, count - N), 0);
    

    如果 count >= NLoopBody 是幂等的,这是第二个首选方案。某些元素可能会被处理两次,但单一代码路径和完全向量化通常是值得的。即使 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<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 内部使用,以非原子方式'混合'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 官方支持的产品。 联系方式:janwas@google.com

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

稿定AI

稿定设计 是一个多功能的在线设计和创意平台,提供广泛的设计工具和资源,以满足不同用户的需求。从专业的图形设计师到普通用户,无论是进行图片处理、智能抠图、H5页面制作还是视频剪辑,稿定设计都能提供简单、高效的解决方案。该平台以其用户友好的界面和强大的功能集合,帮助用户轻松实现创意设计。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号