C 为什么编译器坚持在这里使用被调用方保存的寄存器?
考虑以下C代码:C 为什么编译器坚持在这里使用被调用方保存的寄存器?,c,assembly,gcc,x86-64,register-allocation,C,Assembly,Gcc,X86 64,Register Allocation,考虑以下C代码: void foo(void); long bar(long x) { foo(); return x; } 当我使用-O3或-Os在GCC 9.3上编译它时,我得到以下结果: bar: push r12 mov r12, rdi call foo mov rax, r12 pop r12 ret bar: pu
void foo(void);
long bar(long x) {
foo();
return x;
}
当我使用-O3
或-Os
在GCC 9.3上编译它时,我得到以下结果:
bar:
push r12
mov r12, rdi
call foo
mov rax, r12
pop r12
ret
bar:
push rbx
mov rbx, rdi
imul rdi, rdi
call foo
sub rax, rbx
pop rbx
ret
clang的输出相同,只是选择了rbx
而不是r12
作为被调用方保存的寄存器
但是,我希望/期望看到更像这样的程序集:
bar:
push rdi
call foo
pop rax
ret
由于您无论如何都必须将某些内容推送到堆栈中,因此只将值推送到堆栈中似乎更短、更简单,而且可能更快,而不是将某个被调用方保存的寄存器的值推送到堆栈中,然后将值存储在该寄存器中。当你把东西放回去时,调用foo
后的倒数也是如此
我的装配错误吗?它是否比处理一个额外的寄存器效率更低?如果这两个问题的答案都是“否”,那么为什么GCC或clang不这样做呢
编辑:这里有一个不那么简单的例子,说明即使变量被有意义地使用,它也会发生:
long foo(long);
long bar(long x) {
return foo(x * x) - x;
}
我明白了:
bar:
push r12
mov r12, rdi
call foo
mov rax, r12
pop r12
ret
bar:
push rbx
mov rbx, rdi
imul rdi, rdi
call foo
sub rax, rbx
pop rbx
ret
我宁愿要这个:
bar:
push rdi
imul rdi, rdi
call foo
pop rdi
sub rax, rdi
ret
这一次,它只是一个指令关闭与两个,但核心概念是相同的
.TL:DR:
- 编译器内部可能不是为了方便地寻找这种优化而设置的,它可能只对小函数有用,而不是在调用之间的大函数中
- 大多数情况下,通过内联创建大型函数是更好的解决方案
- 如果
碰巧没有保存/恢复RBX,则可能存在延迟与吞吐量的权衡foo
foo(int)
调用的求值顺序
如果
foo
不保存/恢复rbx
本身,则吞吐量(指令计数)与x
->retval依赖链上的额外存储/重新加载延迟之间存在权衡。
编译器通常倾向于延迟而不是吞吐量,例如,使用2x LEA而不是imul reg,reg,10
(3周期延迟,1/时钟吞吐量),因为在典型的4宽管道(如Skylake)上,大多数代码的平均值明显低于4 uops/时钟。(更多的指令/UOP确实占用了ROB中更多的空间,减少了同一个无序窗口所能看到的前方距离,而且执行实际上是突发性的,暂停可能会导致一些低于4 UOP/时钟的平均值。)
如果foo
确实推送/弹出RBX,那么延迟就没有什么好处了。在ret
之前而不是之后进行恢复可能不相关,除非有ret
预测失误或I-cache未命中延迟在返回地址获取代码
大多数非平凡的函数都会保存/恢复RBX,因此,将变量保留在RBX中实际上意味着它在整个调用过程中真正保留在寄存器中,这通常不是一个好的假设。(尽管有时,随机化保留调用寄存器函数的选择可能是缓解这种情况的一个好办法。)
因此,在这种情况下,是的
push-rdi
/pop-rax
会更有效,这可能是对小型非叶函数的一个遗漏优化,这取决于foo
的功能,以及x
的额外存储/重新加载延迟与保存/恢复调用方的rbx
的更多指令之间的平衡
堆栈展开元数据可以在这里表示对RSP的更改,就像它使用子RSP,8
将x
溢出/重新加载到堆栈插槽中一样。(但是编译器也不知道这种优化,使用push
来保留空间并初始化变量..并且对多个局部变量这样做会导致更大的。eh_frame
堆栈展开元数据,因为每次推送都会单独移动堆栈指针。这不会阻止编译器使用push/p。)保存/恢复的操作调用保留的regs。)
IDK是否值得教编译器寻找这种优化 围绕整个函数,而不是函数内部的一个调用,这可能是一个好主意。正如我所说,它基于一个悲观的假设,
foo
无论如何都会保存/恢复RBX。(或者优化吞吐量,如果您知道从x到返回值的延迟并不重要。但编译器不知道这一点,通常会优化延迟)
如果您开始在大量代码中做出这种悲观的假设(比如函数中的单个函数调用),那么您将开始遇到更多RBX未保存/恢复的情况,您本可以利用这些情况
您也不希望在循环中使用这个额外的save/restore push/pop,只需在循环外保存/恢复RBX,并在进行函数调用的循环中使用保留调用的寄存器。即使没有循环,在一般情况下,大多数函数也会进行多个函数调用。如果在第一次调用之前和最后一次调用之后,在任何调用之间都不使用x
,则可以应用此优化思想,否则,如果在一次调用之后、在另一次调用之前执行一次pop,则每个调用都存在保持16字节堆栈对齐的问题。
一般来说,编译器并不擅长于微小的功能。但是它对CPU也不是很好非内联函数调用在最佳情况下会对优化产生影响,除非编译器能够看到被调用函数的内部结构并做出比通常更多的假设。非inli