"enter"和"push ebp; mov ebp, esp; sub esp, imm"之间的区别,以及"leave"和"mov esp, ebp; pop ebp"之间的区别

62
< p >"enter"和 "return"有什么区别?< code >Enter< /code > 和 < code >Return< /code >的差异是什么?< /p >
push ebp
mov  ebp, esp
sub  esp, imm

有指令和内联函数这两种方法,它们之间有性能差异吗?如果有的话,哪种更快,为什么编译器总是使用后者?

leave 也是一样。

mov  esp, ebp
pop  ebp

说明。


https://dev59.com/K2035IYBdhLWcg3wW-pa?rq=1 - Ciro Santilli OurBigBook.com
4个回答

58

性能方面有差异,尤其是enter。在现代处理器上,这会解码为大约10到20个微操作,而三条指令序列大约为4到6条,具体取决于架构。有关详细信息,请参考Agner Fog的指令表。

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

此外,编译器可能会根据周围代码的情况,将三条指令序列分散开来以进行调度,以允许更多指令的并行执行。


1
请问您从哪里获取这些信息的?leave又是怎么样的呢? - 小太郎
10
请参阅 http://www.agner.org/optimize/microarchitecture.pdf 以了解处理器如何执行代码的全局概述,以及 http://www.agner.org/optimize/instruction_tables.pdf 以获取详细的指令延迟信息。在某些架构中,leave 指令的性能相同,但据我所知,在任何情况下都不会更快。但它确实在指令高速缓存中占用较少的内存。 - Gunther Piez
3
如果三个指令序列比“enter”更快,那么它的意义是什么? - 小太郎
7
兼容性。自8086年以来一直存在,很可能永远存在。同样的情况也适用于“loop”指令:它比“dec reg; jnz”慢得多,但它仍然存在,因为一些旧软件可能会使用它。 - Gunther Piez
9
"Enter/leave"指令在8086/8中没有出现。我认为它们是在80186/8中添加的,因为这些(很少使用的)芯片具有iapx286的所有实模式指令(文档清楚地表明iapx286有enter/leave指令)。 - Brian Knoblauch
1
push ebp / mov ebp, esp / sub esp, imm 指令并不存在3级循环依赖关系。中间的 mov 指令需要从 push 修改后的 ESP 中读取(在英特尔上需要花费一个栈同步 uop),但它本身不会修改 ESP。因此,sub esp, imm 指令可以与 mov 并行执行。自 Pentium-M 以来的堆栈引擎也意味着 push ebp 对于 ESP 并没有单独的延迟成本,它只是从 call 到达这段代码时添加到堆栈偏移量中的另一个堆栈偏移量罢了。 - Peter Cordes

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 n, 0 ; 在486上需要14个时钟周期

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

输入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

输入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

等等,长方法可能会增加文件大小,但速度更快。

最后一个注意点是,程序员现在不再使用display,因为那是一种非常慢的解决方法,使得ENTER变得相当无用。

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


4
“在 486 处理器上,键入“输入 n, 0”会导致循环运行 14 次。但这个示例缺少了“mov bp, sp”的代码行,而且“enter”和“leave”指令是从 186 处理器开始出现的,而不是 286 处理器。” - ecm
2
这里有一个包含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处理器中是全新的。所以我能理解为什么会有混淆。 - Alexis Wilke
1
英特尔的后期 CPU 文档通常假装 186 不存在;它只是用于嵌入式设备,而不是主流 PC,这些 PC 运行与 8086 和 286 PC 相同的应用程序。 - Peter Cordes
1
@AlexisWilke:英特尔的文档通常会忽略186存在的事实;它只是为嵌入式设备而设计,并不适用于与8086和286 PC相同应用程序运行的主流个人电脑。 - Peter Cordes

7
enter在所有CPU上的速度都特别慢,除了可能用于代码大小优化来换取速度之外,没有人使用它。(如果需要帧指针,或者希望允许更紧凑的寻址模式以寻址堆栈空间。) leave足够快,值得使用,GCC也使用它(如果ESP/RSP没有已经指向保存的EBP/RBP,则使用它; 否则,它只使用pop ebp)。
现代英特尔CPU上leave仅为3个微操作,某些AMD上为2个。(https://agner.org/optimize/https://uops.info/)。
对于一个“堆栈引擎”跟踪ESP/RSP更新的现代x86而言,mov/pop总共只有2个微操作。因此,leave比分开执行操作仅多了一个微操作。我曾在Skylake上测试过这一点,在循环中比较了调用/返回和使用mov/popleave设置传统帧指针并拆除其堆栈帧的函数。对于uops_issued.anyperf计数器,使用leave时会多出一个前端微操作,而mov/pop则没有。(我运行了自己的测试,以防其他的测量方法在其leave测量中计算了一个堆栈同步微操作,但在实际函数中使用它可以控制这一点。)
旧CPU可能更喜欢将mov/pop分开执行的一个可能原因:
  • 在大多数没有微操作缓存的CPU中(即Sandybridge之前的英特尔CPU,Zen之前的AMD CPU),多微操作指令可能成为解码瓶颈。它们只能在第一个(“复杂”)解码器中解码,因此可能意味着比正常情况下产生的微操作要少的解码周期。

  • 有些Windows调用约定是被叫方弹出堆栈参数,使用ret n。(例如,在弹出返回地址后,ret 8会使ESP/RSP += 8)。这是一个多微操作指令,与现代x86上的普通近距离ret不同。所以上述原因是双倍的:leave和ret 12不能在同一个周期内解码。

  • 这些原因也适用于构建微操作缓存条目的遗留解码。

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

对于现代 CPU,`leave` 在 UOP 缓存中占用额外的 1 个 UOP。并且这 3 个 UOP 必须在同一行中,可能导致前面一行只填充了部分内容。因此,更大的 x86 代码大小实际上可以改善对 UOP 缓存的装载。或者不行,这取决于如何排列。
节省 2 个字节(或 64 位模式下的 3 个字节)可能值得每个函数多运行 1 个 UOP。
GCC 更喜欢 `leave`,而 clang 和 MSVC 更喜欢 `mov`/`pop`(即使进行 `-Oz` 代码大小优化也是如此,甚至以牺牲速度为代价,例如执行 `push 1 / pop rax`(3 字节)而不是 5 字节的 `mov eax,1`)。
ICC 倾向于使用 mov/pop,但在使用 `-Os` 时将使用 `leave`。https://godbolt.org/z/95EnP3G1f

6
没有真正的速度优势,无论使用哪种方法,尽管长方法可能会更好地运行,因为现在的CPU对更通用的短简单指令进行了更多的“优化”(如果你很幸运,它允许执行端口饱和)。 LEAVE的优点(仍然在使用中,只需查看Windows dlls)是比手动拆卸堆栈帧更小,这在空间有限时非常有帮助。
英特尔指令手册(准确来说是卷2A)将提供更多关于指令的细节,所以可以参考Agner Fogs博士的优化手册

我在我的 Xeon 上测试了 ENTER vs PUSH/MOV,发现它们之间的差异大约是 2 倍。因此,如果您有一个循环超过一百万次的 CALL,它会开始稍微有所不同。我尝试了一亿次迭代,大约需要 1 秒钟。因此,如果您在调用中有 CALL,它会使延迟倍增... 我认为对于编译器来说,这绝对是一个很好的优化。 - Alexis Wilke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接