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个时钟.
此外,三个指令序列可以由编译器展开以用于调度目的,这取决于周围的代码,当然,以允许更多并行执行指令.
小智 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
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或拆除其堆栈帧的函数进行比较leave。perf当您使用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 的子集,甚至根本无法将复杂的指令分解为单独的微指令。
对于现代 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 支持 mov/pop,但 with-Os会使用leave. https://godbolt.org/z/95EnP3G1f
使用它们中的任何一个都没有真正的速度优势,尽管长方法可能会运行得更好,因为现在的 CPU 对更通用的更短、更简单的指令进行了更“优化”(而且它允许执行饱和)如果你幸运的话)。
LEAVE(仍在使用,只需查看Windows dll)的优点是它比手动拆除堆栈帧要小,这在空间有限时有很大帮助。
英特尔指令手册(准确地说是第 2A 卷)将在指令上提供更多细节,Agner Fogs 博士优化手册也应该如此