在 x86 汇编中,是否可以从堆栈中删除一个值而不存储它?类似的东西pop word null
?我显然可以使用add esp,4
,但也许我缺少一个漂亮而干净的 cisc 助记符?
1 回答
add esp,4
/add rsp,8
是正常的/惯用的/干净的方式。不需要特殊的方法,因为堆栈并不神奇或特殊(至少在这方面不是);它只是寄存器中的一个指针,其中包含一些隐式使用它的指令。(对于内核堆栈,中断异步使用它,因此即使软件想要实现内核红区也无法实现......)
除此之外,在函数末尾清理整个堆栈帧的神奇 CISC 方法是leave
= mov esp, ebp
/ pop ebp
(或 16 位或 64 位等效项)。与 不同enter
的是,它在现代 CPU 上足够快,可以在实践中使用,但在 Intel CPU 上仍然是 3 uop 指令。(http://agner.org/optimize/)。但leave
只有在您首先花费额外的指令使用ebp
/制作堆栈帧时才有效rbp
。(通常你不会这样做,除非你需要保留可变数量的堆栈空间,例如push
在循环中创建一个数组,或者相当于 C99 VLA 或alloca
. 或者对于初学者代码,以使访问本地人更容易,或者在SP
不能用于寻址模式的 16 位模式下。)
清理堆栈参数的神奇 CISC 方法是让被调用者使用ret imm16
(花费 1 个额外的 uop)来弹出参数,创建一个调用约定,被调用者清理堆栈。在 caller-pops 调用约定中,没有办法使用这种形式的ret
,但您可以简单地保留堆栈偏移量并用于mov
存储 args 以供下一个函数调用,而不是push
(如果函数需要任何堆栈参数;注册-arg 调用约定通常更有效。)
所以神奇的 CISC 方式在现代 CPU 上没有性能优势,只有较小的代码大小。
您可以使用 2 个原因pop reg
来代替add esp,4
:
- 代码大小:
pop r32/r64
是一个单字节指令,而 . 3 字节add esp,4
或 4 字节add rsp,8
. 性能:当您在堆栈指令(push/pop/call/ret)之后显式使用
esp
/时,英特尔的堆栈引擎必须插入额外的堆栈同步微指令。rsp
因此,在 acall
(返回 a )之后,它会在函数末尾ret
保存一个 uop 以供使用pop
,而不是add esp,4
在你之前使用。ret
AMD 的堆栈引擎不需要额外的堆栈同步微指令,但仍会生成推送/弹出单微指令。与旧的 Intel/AMD CPU 不同,push/pop 的成本高于普通
mov
的加载/存储,需要单独的 uop 来修改堆栈指针。并创建对堆栈指针的数据依赖。
请参阅为什么此函数将 RAX 作为第一个操作推送到堆栈?有关性能的更多详细信息。
如果您正在寻找美学,那么您可以很好地缩进、格式化和注释您的代码,但是如果美学胜过优化,那么当您选择 x86 asm 时,您选择了错误的语言。
当然,如果您需要将堆栈调整超过 1 个寄存器宽度,add
如果您不需要pop
要加载的数据,请务必使用。或者,如果您需要将其调整 +128 字节,请使用sub esp, -128
, 因为-128
可编码为符号扩展的 imm8,但 +128 不是。
或者也许使用lea esp, [esp+4]
,就像 gcc 对-mtune=atom
. (对于有序原子,而不是silvermont)。就像我说的,如果你想要干净,你不应该选择 x86 asm。
您几乎总能找到一个死寄存器pop
进入. 如果您需要在弹出一些您实际想要弹出的寄存器之前将 E/RSP 调整一个堆栈槽,您总是可以弹出同一个寄存器两次。
在极少数情况下,7 (x86-32) 或 15 (x86-64) 非堆栈寄存器都不能用作pop
目标,这种优化不可用,您应该简单地使用传统的add
. 不值得花费额外的说明来使其成为可能pop
;这将超过使用pop
.
请注意,pop Sreg
(段寄存器)仍然消耗常规的“堆栈宽度”(32 位或 64 位,具体取决于模式),而不是 16 位寄存器仅消耗 16 位。 但只有pop ds/es/ss
单字节。 pop fs/gs
每个 2 个字节。因此,如果您正在优化代码大小,pop gs
则 1 字节比 小add esp,4
,但要慢得多。(或比 小 2 个字节add rsp,8
)。