指令集优化
本文整理常见的 CPU 侧优化手段,重点放在 NEON 与 OpenMP 两类方法。前者偏数据级并行,后者偏线程级并行,二者可以结合使用。
1. 基本概念
1.1 SIMD 与 SIMT
SIMD:Single Instruction Multiple Data,单指令多数据,核心是同一条指令同时处理多个数据元素。SIMT:Single Instruction Multiple Threads,单指令多线程,更常见于 GPU 执行模型。
在 CPU 侧讨论指令集优化时,通常关注的是 SIMD。它利用更宽的寄存器和向量指令,在单线程内提升吞吐。
1.2 指令集优化的核心目标
- 减少循环中的标量运算次数
- 提高单次指令处理的数据量
- 减少内存访问带来的开销
- 让编译器更容易生成高效机器码
2. NEON 优化
NEON 是 ARM 平台常见的 SIMD 指令集。以 float32x4_t 为例,一个 128-bit 向量寄存器可以同时处理 4 个 float32。
2.1 使用场景
适合以下类型的计算:
- 点云或图像中的批量坐标变换
- 向量加减乘除
- 矩阵中的逐元素计算
- 一些前处理、后处理阶段的固定公式运算
2.2 注意事项
- 向量化并不等于一定更快,前提是数据排布和访存方式合理。
- 如果使用过多寄存器,可能触发寄存器溢出,编译器会把部分中间变量写回内存,反而降低性能。
- 对结构体数组做向量化时,要特别关注数据访问是否连续。
2.3 常见 NEON 类型与指令
float32x4_t:4 个float32vmulq_n_f32:向量乘标量vmlaq_n_f32:向量乘标量再加到原向量vaddq_f32:向量加法vdupq_n_f32:标量广播为向量vgetq_lane_f32:取出向量某一位的标量值
2.4 示例:点云外参变换
1 | for (int i = 0; i <= N - 4; i += 4) { |
2.5 适合总结的经验点
- 尽量保证数据连续,减少 gather/scatter 风格访问
- 优先处理
N能整除向量宽度的主循环,尾部单独处理 - 关注编译器优化级别和目标平台选项
- 先确认瓶颈在计算,再做 NEON 化
3. OpenMP 优化
OpenMP 解决的是线程级并行问题,适合把一个大的循环任务分给多个 CPU 核心同时执行。
3.1 使用方式
通常不需要额外安装,只需要在编译时打开 -fopenmp。
3.2 常见调度策略
static
特点:
- 循环开始前就把迭代任务分好
- 每个线程拿到一段连续区间
- 分配开销低
适合:
- 每次迭代耗时接近
- 规则型数值计算
- 负载比较均匀的循环
示例:
1 |
|
dynamic
特点:
- 线程处理完当前任务后,再领取下一块任务
- 负载更均衡
- 调度开销更高
适合:
- 每次迭代耗时差异较大
- 某些分支路径明显不均匀的场景
guided
特点:
- 一开始给大块任务
- 后面逐渐减小 chunk
- 兼顾调度成本和负载均衡
适合:
- 任务整体不均衡,但不希望 dynamic 调度成本过高
auto
特点:
- 交给编译器或运行时决定如何调度
3.3 常用 API
omp_get_num_threads()omp_get_thread_num()
3.4 同步与规约
critical 可以保证临界区互斥,但会有明显同步开销。对于求和、最大值这类操作,更推荐使用 reduction。
1 |
|
4. NEON 与 OpenMP 的配合
可以把两者理解为两个不同层次:
NEON:单线程内把一条循环做宽OpenMP:把多次循环分给多个线程做
常见组合方式:
- 外层使用
OpenMP切分数据块 - 每个线程内部再用
NEON做向量化
这种方式通常可以同时利用多核和 SIMD。
5. 实践建议
5.1 优化前先分析
先确认程序瓶颈到底在哪里:
- 是纯计算瓶颈
- 还是缓存/内存访问瓶颈
- 还是线程同步导致的瓶颈
5.2 常见误区
- 数据太小也强行并行,导致线程开销大于收益
- 访存不连续,导致 SIMD 收益很弱
- 用
critical包住大段代码,线程基本串行化 - 没有区分“计算优化”和“调度优化”
5.3 建议的整理顺序
如果后续继续补充内容,可以按下面顺序扩展:
- 标量实现
- NEON 向量化
- OpenMP 多线程
- NEON + OpenMP 组合
- 性能测试与对比