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



enter

push ebp
mov  ebp, esp
sub  esp, imm

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

leave

mov  esp, ebp
pop  ebp

指示。

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

此外,与三个指令序列的 3 个时钟依赖链相比,enter指令通常具有相当高的延迟,例如 core2 上的 8 个时钟。

此外,三个指令序列可以由编译器出于调度目的而分散开来,当然取决于周围的代码,以允许更多的并行执行指令。

在设计 80286 时,英特尔的 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

ENTER 的替代方案是:

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

push    bp              ;1 cycle on the 486
sub     sp, n           ;1 cycle on the 486

;在 486 上输入 n, 1 ;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

;在 486 上输入 n, 3 ;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

等。漫长的方式可能会增加您的文件大小,但速度更快。

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

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

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

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

英特尔说明手册(准确地说是第 2A 卷)将包含更多关于说明的细节,Agner Fogs 博士优化手册也应该如此

enter在所有

CPU上都非常慢,没有人使用它,除了可能以牺牲速度为代价进行代码大小优化。 (如果需要帧指针,或者希望允许更紧凑的寻址模式来寻址堆栈空间。

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

leave在现代英特尔 CPU 上只有 3 uops(在某些 AMD 上为 2 uops)。 (https://agner.org/optimize/,https://uops.info/)。

MOV/POP总共只有2 UOPS(在现代x86上,"堆栈引擎"跟踪ESP/RSP的更新)。 因此,leave只是比单独做事多一个uop。 我已经在 Skylake 上对此进行了测试,将循环中的调用/ret 与设置传统帧指针并使用 mov/popleave 拆除其堆栈帧的函数进行比较。 当您使用 leave 时,uops_issued.anyperf计数器显示的前端 uop 比 mov/pop 多一个。 (我运行了自己的测试,以防其他测量方法在他们的休假测量中计算堆栈同步 uop,但在实际函数中使用它可以控制这一点。

较旧的CPU可能使mov/pop分开受益更多可能的可能原因:

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

  • 一些 Windows 调用约定是被调用方-pops 堆栈参数,使用 ret n 。 (例如 ret 8弹出返回地址后执行 ESP/RSP += 8)。 这是一个多 uop 指令,与现代 x86 上的普通近ret不同。 所以上述原因加倍:离开和ret 12无法在同一周期内解码

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

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

对于现代 CPU,leave 会在 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偏爱移动/流行音乐,但-Os将使用leave。 https://godbolt.org/z/95EnP3G1f

最新更新