根据this PDF document (Page-66),以下几条语句
mov eax, DWORD PTR SS:[esp]
mov eax, DWORD PTR SS:[esp + 4]
mov eax, DWORD PTR SS:[esp + 8]
等同于以下语句:
pop eax
pop eax
pop eax
前者比后者有什么优势吗?
答案 0 :(得分:1)
mov
将数据保留在堆栈上,pop
将其删除,因此您只能按顺序读取一次。除非您使用的调用约定/ ABI在堆栈指针下方包含红色区域,否则ESP以下的数据必须被视为“丢失”。
数据通常 仍在ESP下方,但是可以使用异步信号(例如信号处理程序或在过程上下文中评估call fflush(0)
的调试器)来
此外,pop
修改了ESP,因此每个pop
都需要在可执行文件/库的另一部分中展开堆栈元数据 1 ,因为它完全符合ABI要求Windows上的SEH或其他操作系统上的i386 / x86-64 System V ABI(指定所有功能都需要展开元数据,即使它们不是实际上支持传播异常的C ++功能)也是如此。
但是,如果您是最后一次读取数据,并且确实需要全部数据,那么然后yes pop是一种在现代CPU上读取数据的有效方法(例如Pentium-M和更高版本, a stack engine to handle the ESP updates而没有单独的uop。)
在奔腾III等较旧的CPU上,pop
实际上比3倍的mov
+ add esp,12
慢,并且编译器确实按照Brendan的答案所示的方式生成代码。
void foo() {
asm("" ::: "ebx", "esi", "edi");
}
此函数强制编译器保存/恢复3个保留调用的寄存器(通过在它们上声明Clobbers)。它实际上并不是真正地触摸它们。内联asm字符串为空。但是,这很容易看出编译器将如何进行保存/还原。 (这是他们唯一一次正常使用pop
的时间。)
GCC的默认(tune = generic)代码生成,或者例如使用-march=skylake
,就像这样(from the Godbolt compiler explorer)
foo: # gcc8.3 -O3 -m32
push edi
push esi
push ebx
pop ebx
pop esi
pop edi
ret
但是告诉它调整没有堆栈引擎的旧CPU可以做到这一点:
foo: # gcc8.3 -march=pentium3 -O3 -m32
sub esp, 12
mov DWORD PTR [esp], ebx
mov DWORD PTR [esp+4], esi
mov DWORD PTR [esp+8], edi
mov ebx, DWORD PTR [esp]
mov esi, DWORD PTR [esp+4]
mov edi, DWORD PTR [esp+8]
add esp, 12
ret
gcc认为-march=pentium-m
没有堆栈引擎,或者至少选择不使用push/pop
。我认为这是一个错误,因为Agner Fog's microarch pdf确实将堆栈引擎描述为Pentium-M中的版本。
在PM及更高版本上,push / pop是单联指令,ESP更新是在无序后端之外进行处理的,而对于push来说,存储地址+存储数据是微融合的。
在Pentium 3上,它们分别为2或3 uops。 (再次,请参阅Agner Fog的说明表。)
在有序P5 Pentium上,push
和pop
实际上很好。 (但是通常避免使用诸如add [mem], reg
之类的内存目标指令,因为P5不会将它们拆分为微指令以更好地进行流水线操作。)
在现代Intel CPU上,将pop
直接引用到[esp]
的混合实际上可能比仅使用其中一个慢,因为这会花费额外的堆栈同步指令。
显然,EAX背靠背写了3次,这意味着前两个负载在两个序列中都没有用。
请参阅Extreme Fibonacci,了解pop(1 uop或类似1.1 uop,并已摊销堆栈同步uops)的示例比lodsd(Skylake上为2 uops)读取数组的效率更高。 (在邪恶的代码中假设一个大的红色区域,因为它没有安装信号处理程序。除非您确切地知道自己在做什么以及何时中断,否则请不要这样做;这更多是愚蠢的计算机技巧/对代码高尔夫球进行了极端的优化,而不是实际有用的任何方法。)
脚注1:通常,Godbolt编译器资源管理器会过滤掉多余的汇编器指令,但是如果您取消选中该框,则可以看到在每次push / pop之后,使用push / pop的gcc函数具有.cfi_def_cfa_offset 12
弹出。
pop ebx
.cfi_restore 3
.cfi_def_cfa_offset 12
pop esi
.cfi_restore 6
.cfi_def_cfa_offset 8
pop edi
.cfi_restore 7
.cfi_def_cfa_offset 4
.cfi_restore 7
元数据伪指令必须存在,而与push / pop和mov无关,因为这可以使堆栈展开时在展开时恢复调用保留的寄存器。 (7
是寄存器号。
但是对于函数内部的push / pop的其他用法(例如将args推入函数调用,或使用伪pop
将其从堆栈中删除),您将没有.cfi_restore
,只是堆栈指针的元数据相对于堆栈框架发生了变化。
通常,您不必担心手写asm的问题,但是编译器必须正确解决此问题,因此就可执行文件的总大小而言,使用push/pop
会产生少量额外费用。但是,仅在文件中通常未映射到内存中且未与代码混合的部分中。
答案 1 :(得分:0)
此:
pop eax
pop ebx
pop ecx
..等效于此:
mov eax,[esp]
add esp,4
mov ebx,[esp]
add esp,4
mov ecx,[esp]
add esp,4
..它可能像这样:
mov eax,[esp] ;Do this instruction
add esp,4 ; ...and this instruction in parallel
;Stall until the previous instruction completes (and the value
mov ebx,[esp] ;in ESP becomes known); then do this instruction
add esp,4 ; ...and this instruction in parallel
;Stall until the previous instruction completes (and the value
mov ecx,[esp] ;in ESP becomes known); then do this instruction
add esp,4 ; ...and this instruction in parallel
对于此代码:
mov eax, [esp]
mov ebx, [esp + 4]
mov ecx, [esp + 8]
add esp,12
..所有指令都可以并行发生(理论上)。
注意:实际上,以上所有内容取决于哪个CPU等。