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

为何ARM NEON SIMD求和在M2 Mac上比串行求和慢?

M2 Mac上手动NEON SIMD求和性能低于串行代码的原因分析

在M2 Mac上测试内存带宽时,发现手动实现的ARM NEON SIMD求和代码处理速度远慢于朴素串行求和,内存带宽也更低。测试已开启-O3优化,推测串行代码被编译器自动向量化,希望手动SIMD代码能达到同等性能。


手动NEON SIMD求和代码

int arm_neon_simd_sum(const std::vector<int8_t>& arr) {
    // 检查数组是否为空
    if (arr.empty()) return 0;

    int32x4_t v_sum = vdupq_n_s32(0); // 初始化NEON向量存储部分和
    size_t i = 0;

    // 按16元素块处理数组
    for (; i + 15 < arr.size(); i += 16) {
        int8x16_t v_data = vld1q_s8(&arr[i]); // 将16个元素加载到NEON寄存器

        // 将int8x16_t转换为int16x8_t
        int16x8_t v_data_low = vmovl_s8(vget_low_s8(v_data)); // 低8个元素扩展为int16
        int16x8_t v_data_high = vmovl_s8(vget_high_s8(v_data)); // 高8个元素扩展为int16

        // 转换为int32x4_t并累加
        v_sum = vaddq_s32(v_sum, vpaddlq_s16(v_data_low));
        v_sum = vaddq_s32(v_sum, vpaddlq_s16(v_data_high));
    }

    // 水平相加向量得到所有元素的总和
    int32_t sum_array[4];
    vst1q_s32(sum_array, v_sum);

    int sum = sum_array[0] + sum_array[1] + sum_array[2] + sum_array[3];

    // 处理剩余元素
    for (; i < arr.size(); ++i) {
        sum += arr[i];
    }

    return sum;
}

SIMD测试结果

2024-07-12 21:31:51.101 -------- running arm_neon_simd_sum --------
....................................................................................................
2024-07-12 21:32:03.808 Result: 536870900
2024-07-12 21:32:03.808 Total execution time: 12.705522168999996 seconds
2024-07-12 21:32:03.808 Total bytes processed: 107374182400
2024-07-12 21:32:03.808 Throughput: 7.870593484460515 GB/s

朴素串行求和代码

int sum_sequential(const std::vector<int8_t> &arr) {
  int sum = 0;
  for (int64_t i = 0; i < kArraySize; i++) {
    sum += arr[i];
  }
  return sum;
}

串行测试结果

2024-07-13 10:28:54.706 -------- running sequential sum --------
....................................................................................................
2024-07-12 21:31:02.750 Result: 536870900
2024-07-12 21:31:02.750 Total execution time: 3.303718871999998 seconds
2024-07-12 21:31:02.750 Total bytes processed: 107374182400
2024-07-12 21:31:02.750 Throughput: 30.268919322261286 GB/s
2024-07-12 21:31:02.750 -------- running random access sum -------

核心原因分析

1. 编译器自动向量化的优势

开启-O3后,Apple Silicon默认的Clang编译器会对串行代码做深度优化:

  • 多累加器消除依赖:自动生成多个NEON向量累加器,避免单累加器的指令依赖瓶颈,充分利用CPU超标量执行能力。
  • 高效指令序列:直接使用最优的转换/累加指令,比如将int8元素成对相加并扩展,减少冗余操作;同时自动做循环展开,一次加载更多数据(如32字节),提升内存带宽利用率。
  • 优化水平求和:用vaddvq_s32这类直接计算向量总和的指令,避免手动代码中「向量存内存再相加」的额外开销。

2. 手动SIMD代码的性能瓶颈

  • 单累加器依赖瓶颈:仅用一个int32x4_t累加器,每次循环需等待前一次加法完成,无法充分利用CPU并行执行单元,限制吞吐量。
  • 冗余转换步骤:先通过vmovl_s8扩展int8为int16,再用vpaddlq_s16相加转int32,比直接用vpaddlq_s8(int8成对相加并扩展为int16)的指令数更多,增加计算开销。
  • 低效水平求和:手动将向量写入栈内存再相加,比NEON原生水平求和指令多一次内存读写,增加延迟。
  • 无循环展开:每次仅处理16个int8元素,内存访问粒度太小,无法充分利用M2的内存带宽。

优化建议

  • 使用多累加器:定义4个int32x4_t累加器,循环中分别累加不同数据块,最后合并总和,消除指令依赖。
  • 优化转换逻辑:用vpaddlq_s8直接将int8x16转换为int16x8(成对相加并扩展),再用vpaddlq_s16得到int32x4,减少转换步骤的指令数。
  • 替换水平求和方式:用vaddvq_s32(v_sum)直接计算向量总和,避免内存读写操作。
  • 手动循环展开:一次处理32个int8元素,提升内存访问吞吐量。
  • 确保内存对齐:显式指定std::vector的对齐方式,保证加载指令的执行效率。

内容的提问来源于stack exchange,提问作者Bob Fang

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 23:07:33