返回专辑
·Johan·4 分钟阅读

SIMD 入门:从标量循环到可向量化写法

SIMD 入门:对齐、intrinsics 与点云/图像里值得 vectorize 的 loop。

SIMD 入门:从标量循环到可向量化写法

1. 先 locality,再 SIMD

地面滤波 z 阈值扫描占 profile 30%,直接手写 AVX2——结果比 scalar 还慢:remainder 分支、未对齐 load、cache 未友好。SIMD 是最后一英里,不是第一口。正确顺序:cache-friendly scalar → profiler 确认 hotspot → autovec 报告 → 必要时 intrinsics。数据布局不对时,向量化只是在错误内存模式上跑更快——先保证顺序访问、结构体数组或 SoA 转换,再谈 SIMD。

点云 z 偏移、图像梯度、简单 axpy 是典型数据并行;带大量分支的 per-point predicate 往往要先 compress indices 再 SIMD pass。同样 scalar 循环,编译器 autovec 有时够,有时要 explicit intrinsics(SSE/NEON)。团队规范:无 profile 数据不得合入手写 intrinsics PR。

2. 让编译器 autovec

cpp
void axpy(float a, const float* x, float* y, size_t n) {
  for (size_t i = 0; i < n; ++i)
    y[i] += a * x[i];
}

-O3 -march=native 加 GCC -fopt-info-vec 看是否生成 SIMD;Compiler Explorer 或 objdump -d 核对。热点未进 profile 前别手写 intrinsics——维护成本与 portability 都高。OpenCV cv::dot/gemm 已在优化库内,profile 确认热点在自写 loop 再动手。三重循环 dot product 先 autovec,手写 AVX2 前确认 hot loop 已占 profile 30%+。

3. 对齐与 Eigen

AVX 路径偏好 32B 对齐:alignas(32)std::aligned_alloc。未对齐 load 在 x86 上走慢路径,旧 ARM 可能 fault。Eigen 矩阵运算自动 SIMD,前提 fixed size、对齐、EIGEN_DONT_ALIGN 未滥用——很多 A*B 已在 BLAS,别重复造轮子。Eigen::aligned_allocatorstd::aligned_alloc(32, ...)_mm256_load_ps 用。

4. 手动 intrinsics 的边界

需 handling remainder(scalar epilogue 或 mask load)、x86 与 ARM NEON 两套实现。实验室 -march=native 结果不可直接外推 Jetson Orin——SIMD 内核放 #ifdef __AVX2__ 分支,scalar fallback 必须数值一致。

cpp
#ifdef __AVX2__
  __m256 vz = _mm256_load_ps(&z[i]);
  __m256 mask = _mm256_cmp_ps(vz, _mm256_set1_ps(th), _CMP_LT_OQ);
#endif

-ffast-math 让 autovec 更激进,但与 deterministic replay 冲突——robotics CI 默认关闭,benchmark 标注 flags。ARM 上 -mfpu=neon 与 Jetson 默认 flags 对齐后再比 intrinsics。

5. 分支与 parity 测试

点云里 if (z > th) 阻碍 SIMD;可先分离 predicate mask(AVX2 blend)或两遍:标量筛 index、SIMD 处理稠密块。改完跑 parity test:max abs diff < 1e-6;CI matrix 覆盖 x86 与 aarch64。denormal 数据开 fast-math 曾导致 IMU 积分 drift——别跳过数值验收。

减少分支:点云里 predicate 阻碍 SIMD;先 compress indices 再 SIMD pass,或分离 predicate mask。很多手写 AVX 要处理 remainder tail,用 scalar epilogue 或 mask load。

6. 案例:深度图 z 阈值

640×480 深度图 z 阈值扫描,scalar 改 AVX2 batch 比较后省约 2ms——前提 buffer 32B 对齐、尾元素 scalar epilogue。未对齐 UDP payload 直接 load 反而更慢;先 copy 到 align buffer 或走 Unaligned 路径再比 profile。OpenCV cv::v_float32 封装部分 intrinsics,自定义 layout 仍常手写。该案例说明:SIMD 收益在「hot loop 已确认 + 对齐已解决」后才显现,否则是负优化。

7. 验证流程

cpp
assert(near_equal(scalar_sum(z, n), simd_sum(z, n), 1e-5));

改完跑 ctest -R simd_parity 对比 max abs diff。benchmark both scalar 与 SIMD,看 wall time 而非仅 instruction count。标注编译 flags,-ffast-math 变更需 explicit 评审。

8. 与 OpenCV、PCL 的边界

很多滤波已在 OpenCV/PCL 内 vectorize——profile 确认热点在自写 loop 再 intrinsics。把 OpenCV Mat 数据指针交给 Eigen Map 时,须确认 step/连续性;非连续 Mat 先 clone 或走 OpenCV 算子。自定义 layout(如 interleaved xyz+intensity)常须手写 kernel,这时 parity test 与对齐检查缺一不可。

9. onboard 与 lab 的差异

x86 开发机 autovec 可能生成 AVX-512,Orin 上 NEON 路径不同——CI 须双架构 parity。-march=native 在 Docker 里可能不等于 target board。发布前在 onboard 重跑 hotspot loop 的 benchmark,别只信 lab 数字。denormal、NaN 输入在传感器数据里真实存在,SIMD 与 scalar 对特殊值行为须一致或 explicit 拒绝。手写 kernel 的 portability 成本要在设计文档里写清:谁维护 ARM/x86 两路,谁跑 nightly parity。

10. 团队 review 清单

合入 intrinsics 前:profile 截图、compiler flags、parity 测试名、remainder 策略。禁止「感觉更快」式 PR。与 deterministic replay 冲突的 fast-math 须 explicit 标注并获评审。OpenCV/PCL 已 vectorize 的路径禁止重复造轮子——先 grep 热点在不在自写 loop。

10. 验收

  • scalar 与 SIMD 路径 parity 测试通过。
  • profile 证明热点在目标 loop,wall time 下降。
  • remainder 与未对齐路径有测试,不只在「整齐」尺寸上 benchmark。
  • ARM 与 x86 CI 各跑 parity;lab autovec 结果不外推 onboard。

12. 与 PCL、自研 filter 的选型

地面移除、体素降采样若已在 PCL/Open3D 内优化,自写 SIMD 前先 profile 调用栈。把点云从 PointCloud2 转为 PCL 再算,转换成本可能吃掉 SIMD 收益——整条 path 端到端 benchmark。自定义 filter 确有 hotspot 时,SoA 布局 + autovec 往往先于 intrinsics;intrinsics PR 须附 parity 与 onboard 数字。

每个手写 SIMD 模块在 README 记录最低 CPU 要求、fallback 行为、parity 测试命令。发布镜像 compile flag 与 lab 不一致时,SIMD 分支可能未启用——启动日志打印宏状态有助于 field 对齐。SoA 转换有时比 intrinsics 更先见效:结构体数组改平面 float 数组,autovec 常自动跟上。

← 全部文章

johan's blog