为什么使用PUSH/POP而不是SUB和MOV? [英] Why use push/pop instead of sub and mov?

查看:9
本文介绍了为什么使用PUSH/POP而不是SUB和MOV?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我在https://godbolt.org上尝试不同的编译器时,我注意到编译器生成如下代码是非常常见的:

push    rax
push    rbx
push    rcx
call    rdx
pop     rcx
pop     rbx
pop     rax

我知道每个pushpop做两件事:

  1. 将操作数移入/移出堆栈空间
  2. 递增/递减堆栈指针(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?

但是Pentium-M在前端引入了一个"堆栈引擎",它消除了堆栈操作中的堆栈调整部分,如Push/Call/ret/POP。它有效地以零延迟重命名堆栈指针。请参阅Agner Fog's microarch guideWhat is the stack engine in the Sandybridge microarchitecture?

作为一个大趋势,任何在现有二进制文件中广泛使用的指令都会促使CPU设计人员加快它的执行速度。例如,奔腾4试图让所有人停止使用INC/DEC;但没有奏效;current CPUs do partial-flag renaming better than ever。现代的x86晶体管和功率预算可以支持这种复杂性,至少对于大核CPU(不是Atom/Silvermont)来说是这样。不幸的是,我认为对于sqrtsscvtsi2ss这样的指令的错误依赖关系(对目的地)看不到任何希望。


add rsp, 8这样的指令中显式使用堆栈指针需要Intel CPU中的堆栈引擎插入同步微操作来更新寄存器的无序后端值。如果内部偏移量太大,也是如此。

实际上pop dummy_register比现代CPU上的add rsp, 8add esp,4更有效,因此编译器通常会使用默认调优来弹出一个堆栈槽,例如使用-march=sandybridgeWhy does this function push RAX to the stack as the first operation?

另请参阅What C/C++ compiler can use push pop instructions for creating local variables, instead of just increasing esp once?Re:使用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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆