You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

树莓派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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 07:48:12