"enter"vs"push ebp; mov ebp,esp; sub esp,imm"和"leave"vs"mov esp,ebp; pop ebp"

小太郎*_*小太郎 41 assembly stack

enter和之间的区别是什么?

push ebp
mov  ebp, esp
sub  esp, imm
Run Code Online (Sandbox Code Playgroud)

说明?是否存在性能差异?如果是这样,哪个更快,为什么编译器总是使用后者呢?

以相若方式将leave

mov  esp, ebp
pop  ebp
Run Code Online (Sandbox Code Playgroud)

说明.

Gun*_*iez 39

存在性能差异,尤其对于enter.在现代处理器上,这解码到大约10到20μs,而三个指令序列大约是4到6,具体取决于架构.有关详细信息,请参阅Agner Fog的说明表.

另外,该enter指令通常具有相当高的等待时间,例如,与三个指令序列的3个时钟依赖性链相比,在core2上有8个时钟.

此外,三个指令序列可以由编译器展开以用于调度目的,这取决于周围的代码,当然,以允许更多并行执行指令.

  • 有关处理器执行代码的全局概述,请参见http://www.agner.org/optimize/microarchitecture.pdf;有关详细的指令延迟,请参见http://www.agner.org/optimize/instruction_tables.pdf.`leave`在某些架构上性能相同,但AFAIK在任何情况下都不会更快.但是,它在指令缓存中消耗的内存较少 (6认同)
  • 兼容性.它自8086年以来一直存在,而且最有可能永远存在.`loop`指令也是如此:它比`dec reg慢; jnz`,但它仍然存在,因为一些旧软件可能会使用它. (4认同)
  • 输入/离开不在8086/8中.我相信它们是在80186/8中加入的,因为那些(很少使用的)芯片具有iapx286的所有实模式指令(有很好的记录进入/离开). (4认同)
  • 如果3指令序列比`enter`快,那么它有什么意义呢? (2认同)

小智 9

在设计 80286 时,Intel 的 CPU 设计人员决定添加两条指令来帮助维护显示。

CPU内部的微代码如下:

; ENTER Locals, LexLevel

push    bp              ;Save dynamic link.
mov     tempreg, sp     ;Save for later.
cmp     LexLevel, 0     ;Done if this is lex level zero.
je      Lex0

lp:
dec     LexLevel
jz      Done            ;Quit if at last lex level.
sub     bp, 2           ;Index into display in prev act rec
push    [bp]            ; and push each element there.
jmp     lp              ;Repeat for each entry.

Done:
push    tempreg         ;Add entry for current lex level.

Lex0:
mov     bp, tempreg     ;Ptr to current act rec.
sub     sp, Locals      ;Allocate local storage
Run Code Online (Sandbox Code Playgroud)

ENTER 的替代方案是:

; 输入 n, 0 ;486 上的 14 个周期

push    bp              ;1 cycle on the 486
sub     sp, n           ;1 cycle on the 486
Run Code Online (Sandbox Code Playgroud)

; 输入 n, 1 ;486 上的 17 个周期

push    bp              ;1 cycle on the 486
push    [bp-2]          ;4 cycles on the 486
mov     bp, sp          ;1 cycle on the 486
add     bp, 2           ;1 cycle on the 486
sub     sp, n           ;1 cycle on the 486
Run Code Online (Sandbox Code Playgroud)

; 输入 n, 3 ;在 486 上输入 23 个周期

push    bp              ;1 cycle on the 486
push    [bp-2]          ;4 cycles on the 486
push    [bp-4]          ;4 cycles on the 486
push    [bp-6]          ;4 cycles on the 486
mov     bp, sp          ;1 cycle on the 486
add     bp, 6           ;1 cycle on the 486
sub     sp, n           ;1 cycle on the 486
Run Code Online (Sandbox Code Playgroud)

等等。长距离可能会增加文件大小,但速度要快得多。

最后一点,程序员不再真正使用显示,因为这是一种非常缓慢的解决方法,使得 ENTER 现在变得毫无用处。

资料来源:https ://courses.engr.illinois.edu/ece390/books/artofasm/CH12/CH12-3.html

  • “;在 486 上输入 n, 0 ;14 个周期”示例缺少 `mov bp, sp` 行。而且‘进入’和‘离开’出现在186上,而不是286上。 (4认同)
  • 这是一个[80186指令集的PDF](https://www.jamieiles.com/80186/development-guide.pdf),我们可以在其中找到ENTER和LEAVE指令。有趣的是,我发现[这本286书](http://bitsavers.org/components/intel/80286/210498-001_iAPX_286_Programmers_Reference_1983.pdf)其中说这些指令在80286处理器中是全新的。所以我能理解这种困惑。 (2认同)

Pet*_*des 8

enter在所有 CPU 上都慢得无法使用,除了以牺牲速度为代价的代码大小优化之外,没有人使用它。(如果确实需要帧指针,或者希望允许更紧凑的寻址模式来寻址堆栈空间。)

leave 速度足够快,值得使用,并且 GCC确实使用它(如果 ESP / RSP 尚未指向保存的 EBP/RBP;否则它只使用pop ebp)。

leave在现代 Intel CPU 上仅为 3 uops(在某些 AMD 上为 2 uops)。(https://agner.org/optimize/、https://uops.info/)。

mov / pop 总共只有 2 uops(在现代 x86 上,其中“堆栈引擎”跟踪 ESP/RSP 的更新)。所以leave比单独做事情只多了一个 uop。我已经在 Skylake 上对此进行了测试,将循环中的 call/ret 与设置传统帧指针并使用mov/pop或拆除其堆栈帧的函数进行比较leaveperf当您使用leave 时, for 的计数器uops_issued.any比mov/pop 的计数器多显示一个前端微指令。(我运行了自己的测试,以防其他测量方法在其离开测量中计算堆栈同步 uop,但在实际函数控制中使用它。)

较旧的 CPU 可能因保持 mov / pop 分离而受益更多,可能的原因是:

  • 在大多数没有 uop 缓存的 CPU 中(即 Sandybridge 之前的 Intel、Zen 之前的 AMD),多 uop 指令可能会成为解码瓶颈。它们只能在第一个(“复杂”)解码器中解码,因此可能意味着之前的解码周期产生的微指令比正常情况少。

  • 一些 Windows 调用约定是被调用者弹出堆栈参数,使用ret n. (例如,ret 8弹出返回地址后执行 ESP/RSP += 8)。这是一条多 uop 指令,与ret现代 x86 上的普通指令不同。所以上面的原因是双重的:离开并且ret 12无法在同一个周期中解码

  • 这些原因也适用于构建 uop 缓存条目的传统解码。

  • P5 Pentium 还更喜欢 x86 的类似 RISC 的子集,甚至根本无法将复杂的指令分解为单独的微指令。

对于现代 CPUleave在 uop 缓存中占用 1 个额外的 uop。并且所有 3 个必须位于 uop 缓存的同一行中,这可能导致仅部分填充前一行。因此,更大的 x86 代码大小实际上可以改善 uop 缓存的打包。或不,取决于事情如何排列。

节省 2 个字节(或 64 位模式下为 3 个字节)可能值得也可能不值得每个函数 1 个额外的 uop。

GCC 青睐leave, clang 和 MSVC 青睐mov/ pop(即使clang -Oz以牺牲速度为代价进行代码大小优化,例如执行push 1 / pop rax(3 字节) 而不是 5 字节之类的操作mov eax,1)。

ICC 支持 mov/pop,但 with-Os会使用leave. https://godbolt.org/z/95EnP3G1f


Nec*_*lis 7

使用它们中的任何一个都没有真正的速度优势,尽管长方法可能会运行得更好,因为现在的 CPU 对更通用的更短、更简单的指令进行了更“优化”(而且它允许执行饱和)如果你幸运的话)。

LEAVE(仍在使用,只需查看Windows dll)的优点是它比手动拆除堆栈帧要小,这在空间有限时有很大帮助。

英特尔指令手册(准确地说是第 2A 卷)将在指令上提供更多细节,Agner Fogs 博士优化手册也应该如此