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
/pop
或 leave
拆除其堆栈帧的函数进行比较。 当您使用 leave 时,uops_issued.any
的perf
计数器显示的前端 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