为何ARM架构中间接跳转被编译为两条而非单条指令?
关于GCC/Clang对ARM间接跳转指令生成的疑问
示例代码
#include <stdlib.h> #define NEXT goto **ip++ #define guard(n) asm("#" #n) int main() { static void *prog[] = {&&next1,&&next2,&&next1,&&next3,&&next1,&&next4,&&next1,&&next5,&&next1,&&loop}; void ** ip=prog; int count = 100000000; NEXT; next1: guard(1); NEXT; next2: guard(2); NEXT; next3: guard(3); NEXT; next4: guard(4); NEXT; next5: guard(5); NEXT; loop: if (count) { count--; ip=prog; NEXT; } exit(0); }
编译指令差异
编译后,代码中每个next#处的间接跳转被生成了两条ARM指令:
ldr r2, [r3], #4 mov pc, r2 @ indirect register jump
但从指令集角度,完全可以优化为更紧凑的单条指令:
ldr pc, [r3], #4
核心疑惑
此前查阅GCC Bugzilla讨论得知,Cortex-A8的分支预测器不会将ldr pc, [r3, #0]这类指令识别为函数调用,会导致返回预测错误。但当前代码中的跳转是间接goto,并非函数调用,因此产生两个疑问:
- 这是GCC和Clang的优化遗漏吗?
- 还是单条
ldr pc, [r3], #4指令存在未知性能劣势,导致编译器刻意不生成?
原因分析
编译器选择拆分指令是针对ARM架构分支预测特性的有意设计,而非优化遗漏:
1. 分支预测器的行为绑定
在Cortex-A8及部分ARMv7处理器中,ldr pc, [reg], #offset这类指令会被分支预测器默认判定为函数返回操作(类似bx lr或栈上弹出返回地址的操作),处理器会动用返回栈缓冲器(RSB)来预测跳转目标。如果实际场景是普通间接goto而非函数返回,这种错误的预测归类会导致频繁的分支预测失败,反而大幅降低性能。
2. 保守优化的稳定性
将跳转拆分为ldr加载地址到通用寄存器、再mov pc完成跳转的组合,会让处理器将其识别为普通间接分支,转而使用全局历史缓冲器(GHB)等通用分支预测机制。这种处理方式的预测准确率更稳定,能避免RSB误判带来的性能损失。
3. 强制生成单条指令的方案
如果确实需要生成单条ldr pc指令,可以通过手动内联汇编改写NEXT宏,示例如下:
#define NEXT do { \ asm volatile("ldr pc, [%0], #4" : "+r"(ip)); \ } while(0)
但这种写法需要自行承担分支预测错误带来的性能风险,仅在确认当前场景不会触发RSB误判时适用。
内容的提问来源于stack exchange,提问作者Renee Cousins
相关产品推荐
相关产品推荐

