树莓派5上Neon加速RGB2Gray:128位Q寄存器为何慢于64位D寄存器?
问题:Neon优化RGB转灰度时128位寄存器版本比64位版本慢的原因分析
我用搭载Armv8架构Cortex-A76四核处理器的Raspberry Pi 5,使用OpenCV的cv::cvtColor实现RGB2Gray转换时速度较慢,于是采用neon_arm.h进行性能优化,优化后速度确实有提升,但出现了不符合预期的结果:
- 版本1:使用
vld3_u8()每次加载64位寄存器的uint8x8x3_t数据,100次测试平均耗时10.5ms - 版本2:使用
vld3q_u8()每次加载128位寄存器的uint8x16x3_t数据,100次测试平均耗时12.7ms
处理的图片尺寸为3009x4248,附上代码寻求原因:
void neon_cv_8(cv::Mat &src,cv::Mat &grey,std::size_t size) { uint8_t *p_img = src.data; uint8_t *p_o = grey.data; uint8x8_t r_corr = vdup_n_u8(76); uint8x8_t g_corr = vdup_n_u8(150); uint8x8_t b_corr = vdup_n_u8(30); for(std::size_t i = 0; i<size ; i+=8) { uint8x8x3_t vdata = vld3_u8(p_img); uint16x8_t tmp = vmull_u8(b_corr, vdata.val[0]); tmp = vmlal_u8(tmp, g_corr, vdata.val[1]); tmp = vmlal_u8(tmp, r_corr, vdata.val[2]); uint8x8_t vgray = vshrn_n_u16(tmp, 8); vst1_u8(p_o, vgray); p_o += 8; p_img += 8 * 3; } }//version-1 void neon_cv_16(cv::Mat &src,cv::Mat &grey,std::size_t size) { uint8_t *p_img = src.data; uint8_t *p_o = grey.data; uint16x4_t r_corr = vdup_n_u16(76); uint16x4_t g_corr = vdup_n_u16(150); uint16x4_t b_corr = vdup_n_u16(30); for(std::size_t i = 0; i<size ; i+=2*8) { uint8x16x3_t vdata = vld3q_u8(p_img); uint16x8_t v_b = vmovl_u8(vget_low_u8(vdata.val[0])), v_g = vmovl_u8(vget_low_u8(vdata.val[1])), v_r = vmovl_u8(vget_low_u8(vdata.val[2])); uint32x4_t tmp_low = vmull_u16(b_corr, vget_low_u16(v_b)); uint32x4_t tmp_high = vmull_u16(b_corr,vget_high_u16(v_b)); tmp_low = vmlal_u16(tmp_low,g_corr, vget_low_u16(v_g)); tmp_high = vmlal_u16(tmp_high,g_corr,vget_high_u16(v_g)); tmp_low = vmlal_u16(tmp_low,r_corr, vget_low_u16(v_r)); tmp_high = vmlal_u16(tmp_high,r_corr,vget_high_u16(v_r)); uint8x8_t vgray0 = vqmovn_u16(vcombine_u16(vrshrn_n_u32(tmp_low,8) ,vrshrn_n_u32(tmp_high,8))); v_r = vmovl_u8(vget_high_u8(vdata.val[0])), v_g = vmovl_u8(vget_high_u8(vdata.val[1])), v_b = vmovl_u8(vget_high_u8(vdata.val[2])); tmp_low = vmull_u16(b_corr, vget_low_u16(v_b)); tmp_high = vmull_u16(b_corr,vget_high_u16(v_b)); tmp_low = vmlal_u16(tmp_low,g_corr, vget_low_u16(v_g)); tmp_high = vmlal_u16(tmp_high,g_corr,vget_high_u16(v_g)); tmp_low = vmlal_u16(tmp_low,r_corr, vget_low_u16(v_r)); tmp_high = vmlal_u16(tmp_high,r_corr,vget_high_u16(v_r)); uint8x8_t vgray1 = vqmovn_u16(vcombine_u16(vrshrn_n_u32(tmp_low,8) ,vrshrn_n_u32(tmp_high,8))); vst1q_u8(p_o, vcombine_u8(vgray0,vgray1)); p_o += 16; p_img += 48; } }//version-2
核心原因分析
1. 版本2存在大量指令冗余
版本2中对128位寄存器的拆分与合并操作完全多余:
- 多次调用
vget_low_u8/vget_high_u8拆分寄存器,vmovl_u8扩展位数,vget_low_u16/vget_high_u16再次拆分 - 额外的
vcombine_u16/vcombine_u8合并操作,这些指令会占用CPU执行周期,完全抵消了128位寄存器的吞吐量优势
而版本1的指令链极其紧凑:加载数据→三次乘加→移位→存储,无多余操作,流水线执行效率更高。
2. 数据类型选择不合理
版本2使用uint16x4_t作为校正系数,每次乘加都要处理32位中间结果,而版本1直接用uint8x8_t系数配合vmull_u8(8位×8位→16位)和vmlal_u8(16位累加8位×8位),数据位宽完全贴合计算需求,避免了不必要的位数扩展开销。
3. 内存访问的对齐开销
Cortex-A76的加载/存储单元对对齐访问有优化,版本2每次加载48字节(16×3),若图像数据未按48字节对齐,会触发非对齐访问的额外开销;版本1每次加载24字节(8×3),对齐要求更低,更不容易出现此类问题。
优化后的版本2实现
直接利用128位寄存器的原生指令,避免拆分/合并操作:
void neon_cv_16_optimized(cv::Mat &src,cv::Mat &grey,std::size_t size) { uint8_t *p_img = src.data; uint8_t *p_o = grey.data; // 用128位8位寄存器存储系数,适配vmlalq_u8指令 uint8x16_t r_corr = vdupq_n_u8(76); uint8x16_t g_corr = vdupq_n_u8(150); uint8x16_t b_corr = vdupq_n_u8(30); for(std::size_t i = 0; i < size ; i += 16) { uint8x16x3_t vdata = vld3q_u8(p_img); // 直接使用128位乘加指令,无需拆分 uint16x16_t tmp = vmullq_u8(b_corr, vdata.val[0]); tmp = vmlalq_u8(tmp, g_corr, vdata.val[1]); tmp = vmlalq_u8(tmp, r_corr, vdata.val[2]); // 16位转8位,用饱和移位指令保证结果正确 uint8x16_t vgray = vqshrn_n_u16(tmp, 8); vst1q_u8(p_o, vgray); p_o += 16; p_img += 16 * 3; } }
额外注意事项
- 编译时开启Neon优化:添加
-mfpu=neon -march=armv8-a+crc编译选项,充分利用Cortex-A76的Neon特性 - 保证图像内存对齐:使用
cv::Mat::create时指定对齐参数,或手动分配对齐内存,避免非对齐访问开销 - 循环展开:可手动尝试循环展开2-4次,进一步提升流水线执行效率
内容的提问来源于stack exchange,提问作者illusionaryshelter
相关产品推荐
相关产品推荐

