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
实际上比3x
mov
+
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
当然会改变as
pop
没有改变的标志。你可以用一个等效的
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