ARM 性能优化实战(二):流水线与分支预测

分支预测失败会冲刷流水线,依赖链会卡住乱序执行。这一篇聚焦「流水线与分支预测」维度,用 2 个用例,看清分支与依赖链的真实代价。

说明:代码为节选,聚焦「优化前 vs 优化后」的关键差异;完整可编译运行的用例见 arm-perf/cases/ 目录。

01 · 分支预测:有序 vs 乱序数据

对同一组数据做「大于阈值则写入」的判断。

❌ 原始代码(性能问题所在)

    /* baseline:乱序,分支随机 */
    uint64_t t0 = now_ns();
    for (int i = 0; i < N; i++)
        if (data[i] >= 128) out[i] = data[i];
    uint64_t t1 = now_ns();
    sink_u64((uint64_t)(out[0] + out[N - 1]));
    uint64_t base = t1 - t0;

🔍 慢在哪

乱序数据让「大于阈值」这个判断几乎随机,分支预测器频繁猜错,每次猜错都要冲刷流水线,白白损失几十个周期。

✅ 优化后的代码

    /* optimized:先排序,让分支可预测(只计时写入这段) */
    qsort(data, N, sizeof(int), cmp_int);
    t0 = now_ns();
    for (int i = 0; i < N; i++)
        if (data[i] >= 128) out[i] = data[i];
    t1 = now_ns();
    sink_u64((uint64_t)(out[0] + out[N - 1]));
    uint64_t opt = t1 - t0;

    print_result("branch_prediction", base, opt);
    return 0;
}

🛠 怎么优化

让数据有序化(或把分支改写为无分支形式),使分支高度可预测,预测器能稳定地投机执行。

📊 实测结果

本机 x86_64:基线 5,006,710 ns → 优化 582,911 ns,加速 8.589×。

ARM(aarch64, QEMU):基线 10,452,394 ns → 优化 4,932,762 ns,加速 2.119×。

📱 ARM 视角

ARM 流水线虽比 x86 短,但预测失败仍要付出流水线冲刷的代价;让热路径分支可预测,是 ARM 上性价比极高的一招。

02 · 循环展开与依赖链

标量点积只有一个累加器,浮点加法存在串行依赖。

❌ 原始代码(性能问题所在)

    /* baseline:单个累加器,串行依赖 */
    double s = 0.0;
    uint64_t t0 = now_ns();
    for (int i = 0; i < N; i++) s += a[i] * b[i];
    uint64_t t1 = now_ns();
    sink_f64(s);
    uint64_t base = t1 - t0;

🔍 慢在哪

单个累加器让每次浮点加法都依赖上一次的结果,形成串行依赖链;浮点加法 latency 较长,CPU 无法重叠,只能一路干等。

✅ 优化后的代码

    /* optimized:4 路展开 + 4 个独立累加器 */
    double s0 = 0, s1 = 0, s2 = 0, s3 = 0;
    int i = 0;
    t0 = now_ns();
    for (; i + 3 < N; i += 4) {
        s0 += a[i + 0] * b[i + 0];
        s1 += a[i + 1] * b[i + 1];
        s2 += a[i + 2] * b[i + 2];
        s3 += a[i + 3] * b[i + 3];
    }
    for (; i < N; i++) s0 += a[i] * b[i];
    s = s0 + s1 + s2 + s3;
    t1 = now_ns();
    sink_f64(s);
    uint64_t opt = t1 - t0;

    print_result("loop_unroll", base, opt);
    return 0;
}

🛠 怎么优化

拆成多个独立累加器,打破依赖链,让多次乘加可以并行/乱序执行。

📊 实测结果

本机 x86_64:基线 9,867,203 ns → 优化 5,858,198 ns,加速 1.684×。

ARM(aarch64, QEMU):基线 109,899,528 ns → 优化 85,571,794 ns,加速 1.284×。

📱 ARM 视角

依赖链受指令 latency 限制,与架构关系不大;展开后用满 ARM 的发射宽度,是点积/归约类计算的通用手段。


本系列其余文章见「性能优化」分类,或回到《传统ARM优化的思考》看总述。

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

Scroll to Top