Linux 不使用pop操作读取数据是否有优势?
根据,下面是一系列的陈述Linux 不使用pop操作读取数据是否有优势?,linux,assembly,x86,stack,nasm,Linux,Assembly,X86,Stack,Nasm,根据,下面是一系列的陈述 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 前者比后者有什么优势吗?这是: pop eax pop ebx pop ecx 。。在某种程度上等同于: mov eax,[esp] add esp,4 mov ebx,[esp] add esp,4 mov
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
前者比后者有什么优势吗?这是:
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等。
mov
将数据保留在堆栈上,pop
将其删除,因此您只能按顺序读取一次。ESP下面的数据必须被视为“丢失”,除非您使用的调用约定/ABI在堆栈指针下面包含一个红色区域
数据通常仍然在ESP下面,但是异步的东西,比如信号处理程序,或者在进程上下文中评估调用fflush(0)的调试器,可以对其进行处理
另外,pop
修改ESP,因此每个pop
都需要在可执行文件/库的另一部分中使用堆栈展开元数据1,以便它完全符合Windows上的SEH或其他操作系统上的i386/x86-64 System V ABI(指定所有函数需要解压缩元数据,即使它们不是支持传播异常的C++函数)。
但是,如果您是最后一次读取数据,并且您确实需要所有数据,那么yes pop是在现代CPU上读取数据的有效方法(如奔腾M和更高版本,没有单独的uop) 在较旧的CPU上,如奔腾III,
pop
实际上比3xmov
+add esp慢,12
编译器确实按照Brendan的回答生成了代码
void foo() {
asm("" ::: "ebx", "esi", "edi");
}
此函数强制编译器保存/还原3个保留调用的寄存器(通过在这些寄存器上声明clobber)。它实际上不会触及这些寄存器;内联asm字符串为空。但这使编译器可以轻松查看保存/还原的操作。(这是编译器正常使用pop
的唯一时间。)
GCC的默认(tune=generic)代码gen,例如,使用-march=skylake
,如下()
但是告诉它为没有堆栈引擎的旧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
。我认为这是一个错误,因为明确地将堆栈引擎描述为存在于奔腾-m中
在p-M和更高版本上,push/pop是单uop指令,ESP更新在无序后端之外处理,对于push,存储地址+存储数据uop是微融合的
在奔腾3上,每个都有2或3个UOP(同样,请参阅Agner Fog的指令表)
在order P5 Pentium上,push
和pop
实际上是很好的(但是像add[mem],reg
这样的内存目标指令通常是被避免的,因为P5并没有将它们分割成UOP以更好地进行流水线操作)
在现代英特尔CPU上,将
pop
与直接引用[esp]
混合使用实际上可能比仅使用其中一种方式慢,因为它需要额外的堆栈同步UOP
显然,背对背写EAX 3次意味着前2次加载在两个序列中都是无用的 有关pop(1个计量单位,或类似于1.1个计量单位,堆栈同步计量单位已摊销)比lodsd(Skylake上的2个计量单位)在读取阵列时更高效的示例,请参阅。(在邪恶的代码中,由于没有安装信号处理程序,所以假设有一个大的红色区域。除非您确切知道自己在做什么以及什么时候会中断,否则不要实际这样做;这更像是一个愚蠢的计算机技巧/代码高尔夫的极端优化,而不是任何实际有用的东西。)
脚注1:Godbolt编译器资源管理器通常会过滤掉额外的汇编程序指令,但如果取消选中该框,则可以看到gcc使用push/pop的函数在每次push/pop后都有
。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
无论push/pop还是mov,都必须有.cfi_restore 7
元数据指令,因为这样可以让堆栈展开还原在展开时调用保留的寄存器。(7
是寄存器号)
但对于函数内部的push/pop的其他用途(如将参数推送到函数调用,或使用伪pop
将其从堆栈中删除),您不会有.cfi\u restore
,只有堆栈指针相对于堆栈帧更改的元数据
通常,在手工编写的asm中,您不必担心这一点,但编译器必须做到这一点,因此就可执行文件的总大小而言,使用
push/pop
会有一点额外的成本。但仅限于文件中未正常映射到内存且未与代码混合的部分。@PeterCordes,嗯……很好。@PeterCordes,你为什么要写评论?为什么不完整地回答所有这些评论?因为我不确定问题的背景是什么。我想知道你是否有一些关于pop
vs.mov
的用例,你只需要读一次数据,但不管你是否调整了ESPr一些原因。但您似乎认为pop
更有效,所以您没有阅读早期PPro/Pentium III CPU的旧优化手册,该手册建议避免pop
而支持mov
。这仍然是一个不清楚的问题,没有明显的一般答案,只有一些事情我可以评论。add
当然会改变aspop
没有改变的标志。你可以用一个等效的lea
替换add
,使标志保持不变。现代x86 CPU都有一个堆栈引擎,在无序后端之外处理ESP更新,处理p
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