【发布时间】:2020-08-06 01:50:21
【问题描述】:
考虑这个 C 代码:
void foo(void);
long bar(long x) {
foo();
return x;
}
当我在 GCC 9.3 上使用 -O3 或 -Os 编译它时,我得到了这个:
bar:
push r12
mov r12, rdi
call foo
mov rax, r12
pop r12
ret
除了选择 rbx 而不是 r12 作为被调用者保存的寄存器之外,clang 的输出是相同的。
但是,我希望/期望看到看起来更像这样的程序集:
bar:
push rdi
call foo
pop rax
ret
由于无论如何您都必须将某些内容推送到堆栈,因此将您的值推送到那里似乎更短、更简单并且可能更快,而不是在那里推送一些任意被调用者保存的寄存器的值,然后将您的值存储在该寄存器中。当你把东西放回去时,call foo 之后的倒数也是如此。
我的组装错了吗?它是否比弄乱额外的寄存器效率低?如果这两个问题的答案都是“否”,那么为什么 GCC 或 clang 不这样做呢?
编辑:这是一个不那么简单的例子,表明即使变量被有意义地使用,它也会发生:
long foo(long);
long bar(long x) {
return foo(x * x) - x;
}
我明白了:
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
这一次,只有一条指令对两条,但核心概念是一样的。
【问题讨论】:
-
有趣的错过优化。
-
很可能假设将使用传递的参数,因此您希望保存一个易失性寄存器并将传递的参数保存在一个寄存器中,而不是在堆栈上,因为从寄存器对该参数的后续访问速度更快.将 x 传递给 foo ,您将看到这一点。所以它可能只是他们堆栈框架设置的一个通用部分。
-
很公平,我想你知道我的意思,编译器有用于函数进入和退出的代码,一些代码构建的简单规则可以遵循,如果有一个帧指针,然后构建堆栈帧,如果有嵌套函数,则根据架构需要处理返回地址。如果有传递的参数并且有一个嵌套函数(并且参数的寄存器传递用于此架构或调用约定),则通过将值移出传递的寄存器而不是堆栈来进行设置。您可以为任何规模的任何功能做的事情。
-
通常很容易超越编译器,给定足够大的项目规模,有许多错过的优化(以及像这样的小函数)。编译器在手工 asm 编码器上可以做的是一致性和效率,最好使用高级语言并在需要的地方修复输出,而不是尝试自己在 asm 中编写整个东西只是为了让一些代码更快(但总体上可能没有额外的努力和技巧就无法击败编译器)。我们中的一些人当然可以做到这一点,但值得吗?
-
如果效率/性能是关键,那么您希望编译器做的是内联此函数,而不用担心 foo 调用周围的额外指令。或者,您一开始就永远不会在高级语言中创建这样的函数,因为即使使用 push/pop 也没有效率。
标签: c assembly gcc x86-64 register-allocation