为何简单向量乘法任务中ARM NEON Intrinsics比C++实现慢?
问题描述
对两个float指针指向的数据做逐元素乘法,结果写入第三个指针指向的内存,初始C++实现如下:
void cpp_version (float *a, float *b, float *c, int counter, int dim) { for (int i=0; i<counter; ++i) { for (int j=0; j<dim; ++j) { c[j] = a[j] * b[j]; } } }
基于NEON Intrinsics编写的优化版本如下:
void neon_version (float *a, float *b, float *c, int counter, int dim) { for (int i=0; i<counter; ++i) { for (int j=0; j<dim; j+=4) { float32x4_t _a = vld1q_f32(a+j), _b = vld1q_f32(b+j); vst1q_f32(c+j, vmulq_f32(_a, _b)); } } }
使用NDK Cmake针对Armv8-a架构的Android目标交叉编译,编译命令:
cmake -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI="arm64-v8a" \ -DANDROID_NDK=$NDK \ -DANDROID_PLATFORM=android-22 \ .. make
实际测试耗时结果:
average time of neon: 0.0098 ms average time of c++: 0.0067 ms
疑问:为什么这个简单向量乘法场景下,手写NEON Intrinsics的运行速度比普通C++实现还慢?
核心原因
这个结果是多个典型误区共同导致的:
- 编译器已经对普通C++版本做了更优的自动向量化:这个循环逻辑完全规整、无数据依赖,NDK Release模式默认开启的O2/O3优化会自动把标量循环改写为NEON向量实现,且编译器生成的指令会做寄存器调度、流水线排布,优化程度通常比新手写的基础版NEON代码更好。
- 冗余外层循环被编译器优化消除:两层循环中,内层j循环每次都会完整覆盖c数组的前dim个元素,外层counter次循环的计算结果完全一致、没有额外副作用。编译器处理纯C++代码时可以识别到这个冗余,直接把外层循环删掉,仅执行1次内层计算;但手写NEON版本显式调用了vld1q、vst1q这类内存访问内联,编译器会保守判定这些操作存在副作用,不会删除外层循环,平白多执行了counter-1次无效计算。
- 手写NEON代码优化不足:当前手写版本固定步长为4,既没有处理dim不是4倍数的边界问题,也没有做循环展开。每次循环仅执行1组4元素的load-mul-store操作,指令完全串行,无法隐藏访存延迟,也没法占满CPU的执行流水线。编译器自动向量化时默认会做2~4倍的循环展开,一次处理多组数据,执行效率更高。
- 测试结果无参考性:两个版本的测得耗时都在10微秒以内,这个量级下,函数调用开销、计时器本身的精度误差、CPU缓存冷热状态的影响,已经远大于代码本身的执行效率差异,测出的数值波动完全不能反映真实性能差距。
改进建议
- 先查看编译产出的汇编代码,确认普通C++版本的实际优化结果,验证外层循环是否被消除、是否已经自动生成NEON指令。
- 调整测试用例:把计算规模拉到单次执行耗时至少在毫秒级,测试前提前做缓存预热,排除计时误差和状态干扰,再对比性能。
- 手写NEON优化不要只做最基础的向量指令替换,需要配合循环展开、指令重排、数据预取等手段,才能超过编译器的默认优化效果。
内容的提问来源于stack exchange,提问作者Jersey
相关产品推荐
相关产品推荐

