编译器是免费的优化器,但有些昂贵运算它也无力回天。这一篇聚焦「指令集与编译优化」维度,用 4 个用例,展示如何规避除法、用内建指令、改写递归、内联热路径。
说明:代码为节选,聚焦「优化前 vs 优化后」的关键差异;完整可编译运行的用例见 arm-perf/cases/ 目录。
01 · 规避昂贵运算:除法/取模 vs 位掩码
整数除法/取模是代价较高的运算。
❌ 原始代码(性能问题所在)
/* baseline:运行时取模 */
uint64_t s = 0;
uint64_t t0 = now_ns();
for (uint32_t i = 0; i < N; i++) s += (i % d);
uint64_t t1 = now_ns();
sink_u64(s);
uint64_t base = t1 - t0;
🔍 慢在哪
整数除法/取模是昂贵的运算:无硬件除法器时靠软件移位相减实现,有硬件除法器也要几十个周期。
✅ 优化后的代码
/* optimized:位与掩码(仅当 d 为 2 的幂时等价) */
s = 0;
t0 = now_ns();
for (uint32_t i = 0; i < N; i++) s += (i & mask);
t1 = now_ns();
sink_u64(s);
uint64_t opt = t1 - t0;
print_result("division", base, opt);
return 0;
}
🛠 怎么优化
当除数是 2 的幂时,取模等价于按位与:i % 1024 等价于 i & 1023,用一次 AND 就搞定。
📊 实测结果
本机 x86_64:基线 112,810,221 ns → 优化 21,273,997 ns,加速 5.303×。
ARM(aarch64, QEMU):基线 205,063,947 ns → 优化 161,633,127 ns,加速 1.269×。
📱 ARM 视角
部分 ARM 核没有硬件除法指令,除法完全靠软件实现,代价更高;能用移位/掩码替代时一定要替代。
02 · 位运算:popcount 内建指令
统计 64 位整数中 1 的个数。
❌ 原始代码(性能问题所在)
/* baseline:逐位统计 */
uint64_t s = 0;
uint64_t t0 = now_ns();
for (int i = 0; i < N; i++) s += popcount_naive(data[i]);
uint64_t t1 = now_ns();
sink_u64(s);
uint64_t base = t1 - t0;
🔍 慢在哪
朴素实现逐位循环统计 1 的个数,位数多少就循环多少次,效率极低。
✅ 优化后的代码
/* optimized:内建指令 */
s = 0;
t0 = now_ns();
for (int i = 0; i < N; i++) s += __builtin_popcountll(data[i]);
t1 = now_ns();
sink_u64(s);
uint64_t opt = t1 - t0;
print_result("bit_tricks_popcount", base, opt);
return 0;
}
🛠 怎么优化
用内建函数 __builtin_popcountll,直接映射到硬件指令(x86 的 POPCNT,ARM 的 CNT/VADDV)。
📊 实测结果
本机 x86_64:基线 133,751,038 ns → 优化 11,287,928 ns,加速 11.849×。
ARM(aarch64, QEMU):基线 507,599,953 ns → 优化 55,082,849 ns,加速 9.215×。
📱 ARM 视角
ARM 上 popcount 由 CNT 等 NEON/标量指令组合实现,仍是 O(1) 级别,比逐位循环快一个量级。
03 · 递归 vs 迭代
非尾递归求和,每次调用都要压栈、返回。
❌ 原始代码(性能问题所在)
/* baseline:非尾递归,N 层函数调用 */
uint64_t t0 = now_ns();
double r = sum_rec(N);
uint64_t t1 = now_ns();
sink_f64(r);
uint64_t base = t1 - t0;
🔍 慢在哪
非尾递归每次调用都要保存返回地址与局部状态、压栈出栈;深度一大,调用开销远超计算本身,还可能栈溢出。
✅ 优化后的代码
/* optimized:等价的迭代循环 */
double s = 0.0;
t0 = now_ns();
for (uint32_t i = 1; i <= N; i++) s += f(i);
t1 = now_ns();
sink_f64(s);
uint64_t opt = t1 - t0;
print_result("recursion", base, opt);
return 0;
}
🛠 怎么优化
改成等价的迭代循环,消除函数调用与栈开销。
📊 实测结果
本机 x86_64:基线 2,235,103 ns → 优化 148,120 ns,加速 15.090×。
ARM(aarch64, QEMU):基线 4,033,497 ns → 优化 2,216,285 ns,加速 1.820×。
📱 ARM 视角
RISC 架构寄存器多、调用约定要保存/恢复的寄存器也多,函数调用开销相对更明显,热路径递归尤其要避免。
04 · 热路径函数调用开销:内联
热循环里调用一个极小的函数。
❌ 原始代码(性能问题所在)
/* baseline:真实函数调用 */
uint64_t s = 0;
uint64_t t0 = now_ns();
for (uint32_t i = 0; i < N; i++) s += f_call(i);
uint64_t t1 = now_ns();
sink_u64(s);
uint64_t base = t1 - t0;
🔍 慢在哪
热循环里调用一个极小的函数,call/ret、参数传递、寄存器保存恢复的开销盖过了函数体本身。
✅ 优化后的代码
/* optimized:内联展开 */
s = 0;
t0 = now_ns();
for (uint32_t i = 0; i < N; i++) s += f_inline(i);
t1 = now_ns();
sink_u64(s);
uint64_t opt = t1 - t0;
print_result("inline_hot", base, opt);
return 0;
}
🛠 怎么优化
把函数标记为内联(或让编译器自动内联),把函数体直接展开进循环。
📊 实测结果
本机 x86_64:基线 98,519,118 ns → 优化 12,719,834 ns,加速 7.745×。
ARM(aarch64, QEMU):基线 569,577,022 ns → 优化 80,418,261 ns,加速 7.083×。
📱 ARM 视角
RISC 的调用约定保存/恢复寄存器开销不可忽视,热路径小函数内联是 ARM 上很实在的优化。
本系列其余文章见「性能优化」分类,或回到《传统ARM优化的思考》看总述。
