为什么使用PUSH/POP而不是SUB和MOV? [英] Why use push/pop instead of sub and mov?
问题描述
当我在https://godbolt.org上尝试不同的编译器时,我注意到编译器生成如下代码是非常常见的:
push rax
push rbx
push rcx
call rdx
pop rcx
pop rbx
pop rax
我知道每个push
或pop
做两件事:
- 将操作数移入/移出堆栈空间
- 递增/递减堆栈指针(RSP)
因此,在上面的示例中,我假设CPU实际上正在执行12个操作(6个移动,6个加/减),不包括call
。把加/减结合起来不是更有效率吗?例如:
sub rsp, 24
mov [rsp-24], rax
mov [rsp-16], rbx
mov [rsp-8], rcx
call rdx
mov rcx, [rsp-8]
mov rbx, [rsp-16]
mov rax, [rsp-24]
add rsp, 24
现在只有8个操作(6个移动,2个加/减),不包括call
。为什么编译器不使用此方法?
alu
如果您使用-mtune=pentium3
或-mtune=pentium-m
之前的版本进行编译,GCC将像您想象的那样进行代码生成,因为在那些旧的CPU上,推送/弹出确实会解码为堆栈指针上的单独推荐答案操作以及加载/存储。(您必须使用-m32
或-march=nocona
(64位P4 Prescott),因为那些旧的CPU也不支持x86-64)。Why does gcc use movl instead of push to pass function args?
作为一个大趋势,任何在现有二进制文件中广泛使用的指令都会促使CPU设计人员加快它的执行速度。例如,奔腾4试图让所有人停止使用INC/DEC;但没有奏效;current CPUs do partial-flag renaming better than ever。现代的x86晶体管和功率预算可以支持这种复杂性,至少对于大核CPU(不是Atom/Silvermont)来说是这样。不幸的是,我认为对于sqrtss
或cvtsi2ss
这样的指令的错误依赖关系(对目的地)看不到任何希望。
在
add rsp, 8
这样的指令中显式使用堆栈指针需要Intel CPU中的堆栈引擎插入同步微操作来更新寄存器的无序后端值。如果内部偏移量太大,也是如此。
实际上pop dummy_register
比现代CPU上的add rsp, 8
或add esp,4
更有效,因此编译器通常会使用默认调优来弹出一个堆栈槽,例如使用-march=sandybridge
。Why does this function push RAX to the stack as the first operation?
push
而不是sub rsp, n
/mov
初始化堆栈上的局部变量。在某些情况下,这可能是一种胜利,特别是对于具有小值的代码大小,但编译器不会这样做。
另外,GCC/clang不会编写与您显示的完全相同的代码。
如果他们需要保存函数调用周围的寄存器,他们通常会使用mov
到内存来实现。或mov
到它们保存在函数顶部的调用保留寄存器,并将在结束时恢复。
我从来没有见过GCC或Clang在函数调用之前推送多个被调用清除的寄存器,除了传递堆栈参数。并且绝对不会在之后将多个POP恢复到相同(或不同)寄存器中。函数内的溢出/重新加载通常使用mov。这避免了在循环内推送/弹出的可能性(除了将堆栈参数传递给call
),并且允许编译器进行分支,而不必担心将推送与弹出匹配。此外,它还降低了堆栈展开元数据的复杂性,这些元数据必须为移动RSP的每条指令都有一个条目。(将RBP用作传统的帧指针时,指令计数与元数据和代码大小之间的权衡非常有趣。)
类似的您的代码生成可以通过调用保留的寄存器+一个小函数中的一些reg-reg移动看到,该小函数刚刚调用了另一个函数,然后返回一个__int128
,这是寄存器中的函数arg。因此,需要保存传入的rsi:rdi,才能在rdx:rax中返回。
或者,如果在非内联函数调用后存储到全局函数或通过指针存储,则编译器还需要将函数args保存到调用之后。
这篇关于为什么使用PUSH/POP而不是SUB和MOV?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!