Highway 示例教程:从数组求和到运行时分发,hwy/examples 实战样本库全解
【免费下载链接】highwayPerformance-portable, length-agnostic SIMD with runtime dispatch项目地址: https://gitcode.com/GitHub_Trending/hi/highway
Highway 是一个「性能可移植、长度无关(length-agnostic)的 SIMD 库」,而hwy/examples/目录正是学习它的最佳入口:本仓库用一组由浅入深的可编译示例,覆盖了ScalableTag/FixedTag、循环展开、掩码、交错存储、Gather/Scatter、多线程以及动态分发骨架等全部核心手法。读完本文并跑通这些示例后,你将掌握编写高性能 SIMD 模块的完整套路——从最小求和函数到可复用的 per-target 分发架构,以及如何用 Bazel、CMake 或单个编译器命令编译运行它们。
示例库总览:三级难度的目录结构
hwy/examples/README.md 将目录下的示例分为三类,难度与主题层层递进:
| 类别 | 文件 | 定位 |
|---|---|---|
| Introductory(入门) | sum_array_simple.cc、sum_array_advanced.cc、dot_product_unroll.cc、dot_product_mixed_precision.cc、mandelbrot.cc、baker_mix.cc、matrix_transpose_scatter_gather.cc、stream_triad.cc、game_of_life.cc | 逐个演示一类 SIMD 技巧,适合作为第一份 Highway 代码 |
| Infrastructure(基础设施) | benchmark.cc、profiler_example.cc、skeleton*(skeleton.h/skeleton.cc/skeleton-inl.h/skeleton_test.cc) | 基准测试、内置 profiler、以及带运行时分发的完整模块骨架 |
| Challenge(挑战) | masks_and_logic.cc、ctf_aes.cc | ASCII 艺术渲染器、AES 暴力破解,综合运用掩码逻辑与硬件加速指令 |
此外目录中还包含crc64.cc、float_distribution.cc、sum_hex.cc等未在 README 中逐项展开的补充示例。所有示例都遵循 Highway 的统一组织方式:SIMD 代码放在HWY_NAMESPACE内、通过HWY_BEFORE_NAMESPACE()/HWY_AFTER_NAMESPACE()包裹,再用HWY_DYNAMIC_DISPATCH或HWY_STATIC_DISPATCH在main中调用。
入门示例一:sum_array_simple —— 最小可用模板
sum_array_simple.cc 是整个目录中最小的完整示例:对一个float数组求和,余数部分用简单的标量循环回退。它浓缩了 Highway 的「标准三段式」——定义描述符 D、标签 d、向量类型 V:
float SumArraySIMD(const float* HWY_RESTRICT array, size_t count) { const hn::ScalableTag<float> d; // 描述符:可伸缩标签,宽度取当前目标最大值 using V = hn::Vec<decltype(d)>; V sum = hn::Zero(d); size_t i = 0; const size_t N = hn::Lanes(d); // 运行时向量宽度(lane 数) if (count >= N) { for (; i <= count - N; i += N) { sum = hn::Add(sum, hn::LoadU(d, array + i)); // 非对齐加载 } } float total = hn::ReduceSum(d, sum); // 横向归约到标量 for (; i < count; ++i) { // 标量处理尾数 total += array[i]; } return total; }要点解析:
ScalableTag<float>表示「每个向量装尽可能多的 float」,实际 lane 数在不同 ISA 下不同(SSE4 下为 4、AVX3 下为 16),因此Lanes(d)返回的是运行期值,代码本身是长度无关的;- 使用
LoadU(unaligned load)是因为本例未保证数组对齐,这是最保守也最安全的写法; main中通过HWY_STATIC_DISPATCH(SumArraySIMD)(data.data(), count)调用——静态分发直接选中编译时启用的最佳目标,代码量最少,适合单目标部署(见 sum_array_simple.cc)。
入门示例二:sum_array_advanced —— 4 路展开与 LoadN 尾数处理
sum_array_advanced.cc 展示「更高性能」的求和,README 归纳了它相对简单版新增的三点:
- 循环展开(展开因子 4):用
sum0~sum3四个累加器并行推进,掩盖加法延迟; LoadN+ 隐式FirstN语义:用带长度限制的加载处理尾数,替代标量回退,LoadN会把超出remainder的高位 lane 填 0;- 运行时校验:对 float 与 int32 分别用标量实现做 100 000 次重复对照。
其核心结构(sum_array_advanced.cc)体现了 Highway 推荐的余数处理范式:
// Unroll by 4 to mask latency of adds. if (count >= 4 * N) { for (; i <= count - 4 * N; i += 4 * N) { sum0 = hn::Add(sum0, hn::Load(d, array + i)); sum1 = hn::Add(sum1, hn::Load(d, array + i + N)); sum2 = hn::Add(sum2, hn::Load(d, array + i + 2 * N)); sum3 = hn::Add(sum3, hn::Load(d, array + i + 3 * N)); } } // 剩余整向量用 Duff's device 风格的 switch-fallthrough 消化 size_t num_vectors = (count - i) / N; switch (num_vectors) { case 3: ... [[fallthrough]]; case 2: ... [[fallthrough]]; case 1: ... [[fallthrough]]; case 0: break; } // 最后不足一个向量的部分交给 LoadN V loaded_vec = hn::LoadN(d, array + i, remainder); sum3 = hn::Add(sum3, loaded_vec);两个细节值得注意:
- 展开主循环使用了对齐版
Load(而非LoadU),因为该示例配合hwy::AlignedVector分配内存,注释明确写了 “We know memory is aligned, so we can use Load instead of LoadU”; - 只调用一次
ReduceSum——注释指出归约「uses shuffles」,代价较高,所以先向量级累加再归约一次。
HWY_ONCE块中则演示了完整工程闭环:HWY_EXPORT_T(SumArrayTable, SumArraySIMD<T>)建立类型化分发表,HWY_DYNAMIC_DISPATCH_T(SumArrayTable)在运行期调用「最宽」目标实现;数据填充使用FillBytes+hwy::Unpredictable1()防止编译器把循环优化掉(注释也提醒该函数较昂贵,应少用);浮点结果按1e-1容差校验。计时用hwy::platform::Now(),并直接打印Speedup: x.x x的标量/SIMD 对比。
入门示例三:dot_product_unroll 与混合精度点积
dot_product_unroll.cc 结构上与sum_array_advanced相同(4 路展开 + switch 尾数 +LoadN收尾),但把加法换成了融合乘加MulAdd:
sum0 = hn::MulAdd(hn::Load(d, array1 + i), hn::Load(d, array2 + i), sum0);MulAdd(a, b, c)对应多数 ISA 上的 FMA 指令(a*b + c),在浮点场景下既减少指令数又降低舍入次数;测试部分额外覆盖了double(由HWY_HAVE_FLOAT64宏保护)和int32,并用随机数FillRandom生成输入。
dot_product_mixed_precision.cc 演示 README 所说的类型提升(type promotion):对整型点积,先把窄类型向量提升(promote)到更宽的类型再累加,从而在溢出前获得更大的求和值域——这是处理窄类型归约的典型手段。
图形学示例:mandelbrot 的交错存储、掩码与 FMA
mandelbrot.cc 是一个 512×512 网格的 Mandelbrot 类迭代计算(README 列出它覆盖:交错存储、掩码、循环展开、融合乘加)。源码中的工程选择很能说明问题:
- 为彻底避免尾数处理,断言
x_points/y_points为 512 的倍数(HWY_ASSERT(size_t{0} == x_points % NF)); - 每轮迭代并行推进 4 组迭代点(
ij += 4 * NF内调用 4 次CalculateNextSimd),用NegMulAdd/MulSub/MulAdd拼出复杂迭代公式,即 FMA 的密集使用; - 逃逸判定用布尔掩码:
Gt比较生成mask_escaped,再与「此前未逃逸」掩码And组合,最后用MaskedSetOr把新颜色只写入逃逸点——完全避免按 lane 分支; - 输出 PPM 文件时,把分离的 R/G/B 平面用
StoreInterleaved3交错写成连续的 RGB 像素流。
程序同时跑标量与 SIMD 两套实现,写出mandelbrot_scalar.ppm与mandelbrot_simd.ppm,并用归一化差值< 0.01校验两者一致(mandelbrot.cc)。
数据重排示例:baker_mix 与矩阵转置
baker_mix.cc 演示 Baker's map 风格的数组置换,README 点名的技巧在源码中一一对应:
Setup用hn::Iota(du32, i)生成从i开始的连续整数向量并StoreU落盘——这是向量化生成有序序列的惯用法;LocalMix用LowerHalf/UpperHalf把向量拆上下半区,再StoreInterleaved2(upper, lower, ...)交错写回,实现向量内部的「洗牌」;Diffuse演示跨向量移动:先SlideUpLanes预取头部,循环中用SlideUpLanes+SlideDownLanesOr把相邻向量内容拼接移动(Or变体保证被移出的 lane 用旧值补齐);- 结尾调用
hn::Print(du32, "\nLast diffused vector\n", vec)把向量内容打到 stderr,是调试向量数据的标准工具; - 注意
#if HWY_TARGET != HWY_SCALAR分支:依赖UpperHalf的函数在标量目标下退化为空实现,这是多目标共存的典型写法。
matrix_transpose_scatter_gather.cc 则用非连续访存实现转置:GatherIndex/ScatterIndex按寄存器中的索引向量从分散地址批量加载/存储;尾数部分用LoadN/StoreN与GatherIndexN/ScatterIndexN;步长偏移不查表、不重复乘法,而是预先用Iota生成索引再Mul出一个偏移向量,全程驻留寄存器。
其余两个入门示例:stream_triad.cc 是经典的c[i] = a[i] + s * b[i]流式计算,README 特别强调它演示了多线程划分(配合hwy/contrib/thread_pool使用);game_of_life.cc 是模板计算(stencil),展示Slide1UpOr/Slide1DownOr实现「上/下邻居 + 自身」的模板取值,以及布尔掩码做生老病死判定。
基础设施示例:benchmark、profiler 与 skeleton 模块骨架
benchmark.cc—— benchmark.cc 提供点积与 delta 编码的基准测试,集成 Highway 内置的hwy/nanobenchmark.h微基准框架与hwy/aligned_allocator.h对齐分配,是观察「SIMD 相对标量收益」的现成模板。
profiler_example.cc—— profiler_example.cc 演示内置 profiler:在标注的代码区间(zone)内计时,并自动扣除嵌套子区间的时间,适合定位热点(对应 hwy/profiler.h 的 API)。
skeleton 三件套—— 这是整个目录中最重要的架构级示例,README 称其为「一个完整支持运行时分发的模块示例,外加一个『per-target 头文件』,用于把 SIMD 内联进多个 .cc 文件」。三个文件各承担一种角色:
- skeleton.h:普通头文件,只声明与指令集无关的公共接口
CallFloorLog2/SavedCallFloorLog2,用HWY_DLLEXPORT标记导出符号; - skeleton.cc:动态分发的全部机制集中在这一个
.cc中,按顺序包含关键要素:
#include "hwy/examples/skeleton.h" // 动态分发专用:告诉 foreach_target.h 需要重复包含哪个源文件 #undef HWY_TARGET_INCLUDE #define HWY_TARGET_INCLUDE "hwy/examples/skeleton.cc" #include "hwy/foreach_target.h" // IWYU pragma: keep #include "hwy/highway.h" // 必须放在 foreach_target.h 之后 HWY_BEFORE_NAMESPACE(); namespace skeleton { namespace HWY_NAMESPACE { // 每个目标唯一命名空间,多目标代码可共存 ... // 每目标编译一次的 SIMD 实现(FloorLog2 等) } // namespace HWY_NAMESPACE } // namespace skeleton HWY_AFTER_NAMESPACE(); #if HWY_ONCE // 仅在第一遍(某单一目标)编译时成立 namespace skeleton { HWY_EXPORT(FloorLog2); // 生成分发表 HWY_DLLEXPORT void CallFloorLog2(...) { return HWY_DYNAMIC_DISPATCH(FloorLog2)(in, count, out); // 运行时选最优目标 } HWY_DLLEXPORT void SavedCallFloorLog2(...) { const auto ptr = HWY_DYNAMIC_POINTER(FloorLog2); // 存函数指针避免重复查表 return ptr(in, count, out); } } // namespace skeleton #endif // HWY_ONCE源码注释还给出了按目标选择代码路径的方法:#if HWY_TARGET == HWY_SSE4或能力宏HWY_HAVE_INTEGER64,并用hwy::TargetName(HWY_TARGET)打印当前目标。
- skeleton-inl.h:per-target 头文件。当 SIMD 函数需要被多个
.cc内联引用(而非经函数指针调用)时使用。它的头文件保护写法很特别,是动态分发下的必需品:
// 当「已包含标记」与 HWY_TARGET_TOGGLE 定义状态不一致时才处理 // —— 即每个目标恰好重新包含一次 #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其中MulAddLoop展示了「类型无关 + 宽度无关」函数签名:以const D d为参数推导向量类型,同一份代码在每个目标上都能内联生效。skeleton_test.cc 则验证了CallFloorLog2的输出与标量参考一致。
挑战示例:掩码逻辑与硬件 AES
masks_and_logic.cc 是一个 ASCII 艺术渲染器,README 列出其覆盖点:寄存器内分支与掩码、掩码布尔运算(And、AndNot)、用连续IfThenElse表达嵌套条件,以及用带HWY_ATTR标记的 lambda 封装 SIMD 操作——这让多目标函数能以泛型方式被调用。
ctf_aes.cc 是暴力猜密码程序,展示:
FixedTag:与硬件最大宽度无关的定长向量(固定 N 个 lane),适合 AES 这类要求固定 128-bit 语义的算法;- 可移植的硬件加速 AES 轮函数
AESRound(在支持 AES-NI 的目标上落到指令,否则软件实现); FirstN生成前 n 个 lane 为真的掩码,配合MaskedEq做掩码化比较,从而一次判定多个候选密码。
如何运行:Bazel / CMake / 单编译器三种方式
README 的 “How to Run” 给出三条路径,这里原样保留并补充实测要点。
使用 Bazel
在 highway 仓库根目录下(其余目标名见根 BUILD 文件):
bazel run //::sum_array_simple bazel run //::sum_array_advanced # etc; see BUILD file for the other build targets使用 CMake
CMake 构建中示例默认开启:CMakeLists.txt 中set(HWY_ENABLE_EXAMPLES ON CACHE BOOL "Build examples"),各可执行目标(如sum_array_simple,链接hwy与原子库)定义在 CMakeLists.txt 起的HWY_ENABLE_EXAMPLES分支内。从 highway 根目录:
mkdir build && cd build cmake .. -DHWY_ENABLE_EXAMPLES=ON make ./examples/sum_array_simple ./examples/sum_array_advanced # etc; see CMakeLists.txt for the other build targets使用 Clang 直接编译
不借助构建系统时,只需把示例源文件与 Highway 的几个运行时支持.cc一起编译(在 highway 根目录下):
clang++ -std=c++17 -O3 -I. hwy/examples/sum_array_simple.cc hwy/targets.cc hwy/per_target.cc hwy/print.cc hwy/abort.cc hwy/aligned_allocator.cc -o sum_array_simple ./sum_array_simple使用 GCC 直接编译
g++ -std=c++17 -O3 -I. hwy/examples/sum_array_simple.cc hwy/targets.cc hwy/per_target.cc hwy/print.cc hwy/abort.cc hwy/aligned_allocator.cc -o sum_array_simple ./sum_array_simple注意:README 提醒g++可能输出类似no SFrame FDE emitted的汇编器告警,这些是无害的,可以忽略。
几个适用于上述所有方式的适用前提:
- 编译参数需 C++17(
-std=c++17)与优化(-O3),-I.使#include "hwy/highway.h"相对仓库根目录解析; - 需要动态分发的示例(带
HWY_TARGET_INCLUDE+foreach_target.h,如sum_array_advanced.cc、baker_mix.cc)必须编译进hwy/targets.cc、hwy/per_target.cc;而仅用HWY_STATIC_DISPATCH的sum_array_simple.cc依赖更少,这也是 README 直接编译示例选它的原因; - 需要
Print的示例(如baker_mix.cc)额外链接hwy/print.cc;使用AlignedVector的示例需要hwy/aligned_allocator.cc。
小结
hwy/examples/目录实质上是一条完整的学习路径:sum_array_simple建立「D/d/V + 循环 + 归约」的基本功;sum_array_advanced与dot_product_unroll补上展开、LoadN尾数与 FMA 的性能要素;mandelbrot、baker_mix、matrix_transpose_scatter_gather、game_of_life、stream_triad分别覆盖掩码、数据重排、Gather/Scatter、模板计算与多线程;skeleton系列则给出可直接套用的多目标分发模块架构。建议按「先读源码、再用 CMake 一键构建、最后用单条g++/clang++命令手动编译验证依赖」的顺序推进,配合 hwy/highway.h 的 API 与 g3doc/tutorial.md 的教程文档,即可在任意支持 Highway 的目标架构上编写并验证自己的 SIMD 代码。
【免费下载链接】highwayPerformance-portable, length-agnostic SIMD with runtime dispatch项目地址: https://gitcode.com/GitHub_Trending/hi/highway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考