TL:DR:
- 编译器内部可能没有很好地设置这种优化,而且它可能只在小函数内部有用,而不是在大函数内部和调用之间。
- 创建大型函数的内联通常是更好的解决方案
- 如果
foo
恰好未保存/恢复 RBX,则可能存在延迟与吞吐量的权衡。
编译器是复杂的机器。它们不像人类一样“聪明”,而在寻找每种可能的优化时花费昂贵的算法通常不值得在额外的编译时间上付出代价。
我在2016年报告了 GCC bug 69986 - smaller code possible with -Os by using push/pop to spill/reload;GCC 开发人员没有任何活动或回复。 :/
稍相关:GCC bug 70408 - reusing the same call-preserved register would give smaller code in some cases - 编译器开发人员告诉我,为了能够进行这种优化,需要根据使目标汇编更简单的原则选择两个 foo(int)
调用的评估顺序,这需要耗费巨大的工作量。
如果foo本身不保存/恢复rbx,则在吞吐量(指令数量)与在x-> retval依赖链上额外存储/重新加载延迟之间存在权衡。
编译器通常偏向于延迟而非吞吐量,例如使用2x LEA而不是imul reg,reg,10(3个周期的延迟,1/clock吞吐量),因为大多数代码在典型的4宽流水线(如Skylake)上平均显着少于4个uops / clock。(更多指令/uops确实占用了ROB中的更多空间,降低了相同乱序窗口能够看到多远,尽管执行实际上是突发性的,停顿可能占其中一些少于4个uops/clock的平均值。)
如果foo推入/弹出RBX,则延迟没有太多收益。除非有ret误判或I-cache错失导致延迟获取返回地址处的代码,否则在ret之前恢复可能并不重要。
大多数非平凡函数都会保存/恢复RBX,因此让变量保留在RBX中并不意味着它真正跨调用保留寄存器。(尽管有时随机选择函数选择哪些调用保留寄存器可能是一个好主意来减轻这种情况。)
所以在这种情况下,
push rdi
/
pop rax
更有效率,这可能是对于小型非叶函数的一个被忽视的优化,具体取决于
foo
的作用以及额外存储/重新加载
x
与保存/恢复调用者的
rbx
之间的平衡。
在这里,堆栈展开元数据可以表示RSP的更改,就像如果它使用sub rsp, 8
将x
溢出/重新加载到堆栈插槽中一样。(但编译器也不知道使用push
来保留空间和初始化变量的这种优化。哪个C/C++编译器可以使用push pop指令创建本地变量,而不仅仅是增加esp一次?如果对多个本地变量都这样做,会导致更大的.eh_frame
堆栈展开元数据,因为每次推送时都要分别移动堆栈指针。但这并不妨碍编译器使用push/pop来保存/恢复调用保留寄存器。)
我不确定是否值得教编译器寻找这种优化
也许在整个函数范围内是一个好主意,而不是在函数内部的一个调用中。就像我说的,它基于一种悲观的假设,即foo
无论如何都会保存/恢复RBX。(或者在您知道从x到返回值的延迟不重要时优化吞吐量。但编译器不知道这一点,通常会优化延迟)。
如果您在大量代码中开始做出这种悲观的假设(例如在函数内部的单个函数调用周围),则会出现更多情况,其中未保存/恢复RBX,并且可以利用该情况。
您还不希望在循环中进行额外的保存/恢复push/pop操作,只需在循环外保存/恢复RBX并在进行函数调用的循环中使用调用保留寄存器。即使没有循环,在一般情况下,大多数函数进行多次函数调用。如果您确实不在任何调用之间使用x
,仅在第一个调用之前和最后一个调用之后使用,则此优化想法可能适用,否则您将面临每个call
维护16字节堆栈对齐的问题,如果在调用之后进行一次pop操作,再进行另一个调用。
编译器通常不擅长处理微小函数。但它也不适合CPU。最好情况下,非内联函数调用会对优化产生影响,除非编译器可以看到被调用者的内部并做出比通常更多的假设。非内联函数调用是一个隐式的内存屏障:调用者必须假设函数可能读取或写入任何全局可访问数据,因此所有这些变量都必须与C抽象机器同步。(逃逸分析允许在地址未逃逸函数中跨调用保留本地寄存器。)此外,编译器必须假定调用破坏的寄存器都已被破坏。这对于x86-64 System V中没有调用保留XMM寄存器的浮点运算来说是很糟糕的。
像bar()
这样的微小函数最好内联到其调用者中。使用-flto
编译,以便在大多数情况下甚至可以跨文件边界进行内联。(函数指针和共享库边界可能会破坏此功能。)
我认为编译器没有尝试做这些优化的一个原因是需要在编译器内部编写一堆不同的代码,与普通的堆栈与寄存器分配代码不同,它知道如何保存调用保留寄存器并使用它们。
换句话说,实现这个功能需要大量的工作和代码维护。如果过度热衷于此,可能会产生更差的代码。
还有就是(希望)这不是很重要;如果它很重要,你应该将bar
内联到其调用者中,或将foo
内联到bar
中。除非有很多不同的类似bar
的函数且foo
很大,并且由于某种原因它们无法内联到它们的调用者中,否则这是可以接受的。