GCC编译插入冗余寄存器间mov指令的原因技术问询
问题背景
原始C源代码如下:
#define BITSET_WORDIDX(idx) (idx >> 6) #define BITSET_WORD_BITIDX(idx) (idx & 0x3F) static inline __attribute__((always_inline, regparm(2))) unsigned int test_and_set_bit_nonatomic( uint64_t *restrict bitset, const unsigned int bitset_idx) { bitset += BITSET_WORDIDX(bitset_idx); register uint64_t u64_bitset = *bitset; register const uint64_t u64_idx = BITSET_WORD_BITIDX(bitset_idx); register unsigned int cf_copy; asm volatile("btsq %2, %1" : "=@ccc"(cf_copy), "+r"(u64_bitset) : "r"(u64_idx)); *bitset = u64_bitset; return cf_copy; }
使用编译参数 gcc -Ofast -march=native -S -fverbose-asm 编译得到函数内联后对应的汇编代码(已调整注释提升可读性,英文注释已翻译):
# 下行的"%eax"表示"bitset_idx"参数来自外层作用域的寄存器变量 # (因为这是always_inline属性的内联函数) # 当前作用域内不会对"%eax"执行写操作 movl %eax, %edx # (临时寄存器0)bitset_idx = bitset_idx; movl %eax, %ebp # (临时寄存器1)bitset_idx = bitset_idx; shrl $6, %edx # (临时寄存器0)bitset_idx >>= 6; andl $63, %ebp # (临时寄存器1)bitset_idx &= 0x3F; # 下行的"%r13"表示"bitset"参数来自外层作用域的寄存器变量 # (因为这是always_inline属性的内联函数) # 当前作用域内不会对"%r13"执行写操作 leaq 0(%r13,%rdx,8), %rsi # bitset指针偏移,偏移量为(临时寄存器0存储的值 * 8) movl %ebp, %edi # u64_idx = 临时寄存器1存储的位索引值 movq (%rsi), %rdx # u64_bitset = *bitset #APP btsq %rdi, %rdx #NO_APP movq %rdx, (%rsi) # *bitset = u64_bitset
核心疑问
为什么GCC会插入 movl %ebp, %edi # u64_idx = 临时寄存器1存储的位索引值 这条指令?EBP寄存器本身已经存储了计算得到的u64_idx对应值,直接复用EBP作为u64_idx的存储寄存器效率更高,GCC为何没有选择这种寄存器分配方式?
原因解答
这条寄存器搬运指令是内联场景下的寄存器约束规则+GCC分配器保守策略共同导致的,核心原因有三点:
- EBP属于特殊用途的被调用者保存寄存器,分配优先级极低
x86-64 System V ABI规范中,%rbp默认作为帧指针寄存器使用,同时归类为被调用者保存(callee-saved)寄存器:如果外层宿主函数(内联展开的目标函数)已经用%rbp存储了其他长期生效的值,GCC不能随意把内联汇编的操作数分配到%rbp,否则会额外产生压栈保存、出栈恢复的开销,甚至打乱帧指针的正常使用。
代码里%ebp存位索引只是指令选择阶段的临时中间结果分配,到内联汇编操作数分配环节,分配器默认不会优先选%rbp这类有固定用途的寄存器。 - 通用寄存器约束
"r"会优先匹配无额外开销的调用者保存寄存器
内联汇编中u64_idx的约束是"r",即允许GCC选择任意通用寄存器。GCC的寄存器分配优先级中,%rdi/%rsi/%rdx/%rcx/%r8/%r9这类调用者保存(caller-saved)寄存器优先级远高于被调用者保存寄存器:这类寄存器可以直接覆盖使用,不需要提前保存原值,对整个函数的寄存器开销最小。
这段代码里%rdi在btsq指令执行前处于空闲状态,分配器直接把内联汇编输入操作数的寄存器指派给%rdi,自然生成了从%ebp拷贝值到%edi的指令。 - 帧指针省略策略的保守性进一步降低了EBP的分配概率
即使开启-Ofast优化,多数发行版的GCC默认不会全局开启帧指针省略,%rbp始终保留作为帧指针的可能性存在,分配器不会冒险把传入内联汇编的操作数放在%rbp中,避免和帧指针功能冲突。
补充说明:如果硬编码把
u64_idx的约束改成"b"(指定分配到BP寄存器类),GCC确实不会生成这条mov指令,但这种硬编码寄存器的写法可移植性极差,不建议生产环境使用。而且这条mov指令在现代x86处理器上不会产生实际执行开销:CPU的寄存器重命名引擎会直接消除寄存器间的零延迟移动指令,不会占用执行端口、不会产生额外延迟。
内容的提问来源于stack exchange,提问作者hurryman2212
相关产品推荐
相关产品推荐

