Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/c/57.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C 为什么编译器坚持在这里使用被调用方保存的寄存器?_C_Assembly_Gcc_X86 64_Register Allocation - Fatal编程技术网

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

考虑以下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:
        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:

  • 编译器内部可能不是为了方便地寻找这种优化而设置的,它可能只对小函数有用,而不是在调用之间的大函数中
  • 大多数情况下,通过内联创建大型函数是更好的解决方案
  • 如果
    foo
    碰巧没有保存/恢复RBX,则可能存在延迟与吞吐量的权衡
编译器是复杂的机器部件。它们不像人类那样“聪明”,而且寻找每一种可能的优化的昂贵算法往往不值得花费额外的编译时间

我在2016年报告了这一点;GCC开发人员没有任何活动或回复:/

稍微相关:-编译器开发人员告诉我,GCC需要大量工作才能完成优化,因为它需要根据使目标asm更简单的内容选择两个
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