Neon 与 SIMD:让 CPU 一次处理多份像素数据
普通代码通常一次处理一个数。SIMD 的思想是:一条指令同时处理多个数。Neon 是 Arm 平台上的 SIMD 指令扩展。
这篇先建立直觉,再讲什么时候值得用 Neon。
SIMD 的直觉
假设你要把 16 个像素都加亮。
普通标量代码像一个人一次搬一个箱子。
处理像素 0
处理像素 1
处理像素 2
...
SIMD 像一辆能同时搬 8 个箱子的推车。
一次处理像素 0..7
一次处理像素 8..15
如果数据足够规则,SIMD 可以显著提升吞吐。
Neon 在 Android 上的地位
Android NDK 支持 Arm Neon。官方架构文档说明,Arm64 Android 设备都支持 Neon;现代 32 位 Arm 设备也基本支持,NDK 对 Arm ABI 默认启用 Neon。
这意味着在 arm64-v8a 上,你通常可以放心使用 Neon。
适合 SIMD 的代码
YUV 到 RGB
图像滤镜
音频采样缩放
矩阵、向量计算
批量 clamp、add、multiply
不适合 SIMD 的代码:
大量 if/else 分支
数据不连续
每个元素处理逻辑不同
瓶颈在 I/O 或锁,不在计算
所以用 Neon 之前,先用 simpleperf 确认热点确实在纯计算函数。
标量版本
以一个简单的亮度提升为例。
void brightenScalar(uint8_t* data, size_t count, uint8_t delta) {
for (size_t i = 0; i < count; ++i) {
int value = data[i] + delta;
data[i] = static_cast<uint8_t>(value > 255 ? 255 : value);
}
}
它一次处理一个字节。
Neon 版本
#include <arm_neon.h>
void brightenNeon(uint8_t* data, size_t count, uint8_t delta) {
size_t i = 0;
uint8x16_t deltaVec = vdupq_n_u8(delta);
for (; i + 16 <= count; i += 16) {
uint8x16_t pixels = vld1q_u8(data + i);
uint8x16_t result = vqaddq_u8(pixels, deltaVec);
vst1q_u8(data + i, result);
}
for (; i < count; ++i) {
int value = data[i] + delta;
data[i] = static_cast<uint8_t>(value > 255 ? 255 : value);
}
}
逐步解释:
vdupq_n_u8:把 delta 复制成 16 个 lane
vld1q_u8:从内存加载 16 个 uint8
vqaddq_u8:饱和加法,超过 255 自动夹住
vst1q_u8:把结果写回内存
最后的 for:处理不足 16 个的尾巴
为什么要处理尾巴
数据长度不一定刚好是 16 的倍数。SIMD 主循环一次处理 16 个,剩下的几个仍然要用标量处理。
count = 34
SIMD 处理 0..31
标量处理 32..33
漏掉尾巴就是图像末尾错误,甚至越界。
性能不一定总会提升
Neon 不是魔法。它有前提。
数据连续
对齐良好或加载成本可接受
计算占比足够高
不会被内存带宽卡住
不会引入大量格式转换
如果 simpleperf 显示热点是 memcpy 或 pthread_mutex_lock,写 Neon 通常没意义。
可移植策略
建议保留标量基线。
void brighten(uint8_t* data, size_t count, uint8_t delta) {
#if defined(__ARM_NEON)
brightenNeon(data, count, delta);
#else
brightenScalar(data, count, delta);
#endif
}
这样 x86_64 模拟器或非 Arm 构建也能运行。
本章实验
用同一张 1080p Y 平面数据测试。
标量版本耗时
Neon 版本耗时
输出是否逐字节一致
simpleperf 中该函数占比是否下降
正确性比性能优先。SIMD 最容易出现“快但错一点”的问题。
小白常见误解
第一,Neon 不会自动让所有代码变快。它只适合规则、连续、批量的数据。
第二,SIMD 代码更难读,所以必须保留标量版本作为正确性基线。
第三,模拟器 x86_64 不一定能覆盖 Arm Neon 路径。真机测试不可省。
第四,尾部处理非常重要。遗漏尾部不是性能问题,而是正确性 bug。
工程风险与观测
Neon 优化要按能力隔离。
标量路径:所有 ABI 都可运行
Neon 路径:Arm ABI 使用
运行时或编译时开关:异常时可降级
建议记录:
abi
neon_enabled
input_size
scalar_time_ms
neon_time_ms
output_checksum
output_checksum 用来防止“快但错”。如果图片肉眼看着差不多,但 checksum 不一致,就要进一步检查饱和、舍入、边界处理。
上线风险:
某些 32 位设备能力差异
未处理尾部导致越界
内存未对齐导致性能退化
算法结果和标量基线不一致
这些都应该在发布前审计。
本章结论
Neon 的本质是让 CPU 一次处理多份规则数据。它适合像素、音频、矩阵这类连续批处理场景。使用前先采样确认热点,使用后必须做正确性对比和多 ABI fallback。