分支预测失败会冲刷流水线,依赖链会卡住乱序执行。这一篇聚焦「流水线与分支预测」维度,用 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优化的思考》看总述。
