enter
和
push ebp
mov ebp, esp
sub esp, imm
指令?是否存在性能差异?如果是这样,哪个更快,为什么编译器总是使用后者?
与leave
和
mov esp, ebp
pop ebp
的指令。
答案 0 :(得分:35)
性能存在差异,尤其是enter
。在现代处理器上,这解码到大约10到20μs,而三个指令序列大约是4到6,具体取决于架构。有关详细信息,请参阅Agner Fog's指令表。
此外,enter
指令通常具有相当高的延迟,例如core2上的8个时钟,而三个指令序列的3个时钟依赖链。
此外,编译器可以为调度目的分散三个指令序列,具体取决于周围的代码,以允许更多并行执行指令。
答案 1 :(得分:4)
使用它们中的任何一个都没有真正的速度优势,尽管长方法可能会运行得更好,因为现在CPU更加“优化”到更简单的使用更简单的指令(加上它允许饱和)如果你的幸运,执行端口)。
LEAVE
(仍在使用,只看到窗户dll)的优点是它比手动拆除堆叠框架小,这在你的空间有限时会有很大的帮助。
英特尔指导手册(准确地说是第2A卷)将在说明书中包含更多细节,因此Dr Agner Fogs Optimization manuals
答案 2 :(得分:3)
在设计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
答案 3 :(得分:1)
enter
在所有 CPU 上都慢得无法使用,除了可能以牺牲速度为代价进行代码大小优化之外,没有人使用它。 (如果根本需要一个帧指针,或者希望允许更紧凑的寻址模式来寻址堆栈空间。)
leave
足够快,值得使用,并且 GCC 确实使用它(如果 ESP / RSP 不是已经指向保存的 EBP/RBP;否则它只使用 pop ebp
)。
leave
在现代 Intel CPU 上只有 3 个 uops(在某些 AMD 上只有 2 个)。 (https://agner.org/optimize/,https://uops.info/)。
mov / pop 总共只有 2 uops(在现代 x86 上,“堆栈引擎”跟踪对 ESP/RSP 的更新)。所以 leave
只是比单独做事情多一个 uop。我已经在 Skylake 上对此进行了测试,将循环中的 call/ret 与设置传统帧指针并使用 mov
/pop
或 leave
拆除其堆栈帧的函数进行了比较。当您使用 leave 时,perf
的 uops_issued.any
计数器比 mov/pop 多显示一个前端 uop。 (我运行了我自己的测试,以防其他测量方法在其休假测量中计算堆栈同步 uop,但在实际功能控件中使用它。)
较旧的 CPU 可能在保持 mov / pop 分离方面受益更多的可能原因:
在大多数没有 uop 缓存的 CPU(即 Sandybridge 之前的 Intel,Zen 之前的 AMD)中,多 uop 指令可能是解码瓶颈。它们只能在第一个(“复杂”)解码器中进行解码,因此可能意味着之前的解码周期产生的 uops 比正常情况少。
一些 Windows 调用约定是 callee-pops 堆栈参数,使用 ret n
。 (例如,ret 8
在弹出返回地址后执行 ESP/RSP += 8)。这是一个多 uop 指令,与现代 x86 上的 ret
附近的普通指令不同。所以上面的原因是双重的: leave 和 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
代码大小优化,例如做类似 { {1}}(3 个字节)而不是 5 个字节的 push 1 / pop rax
)。
ICC 支持 mov/pop,但使用 mov eax,1
将使用 -Os
。 https://godbolt.org/z/95EnP3G1f