【问题标题】:"enter" vs "push ebp; mov ebp, esp; sub esp, imm" and "leave" vs "mov esp, ebp; pop ebp""enter" vs "push ebp; mov ebp, esp; sub esp, imm" 和 "leave" vs "mov esp, ebp; pop ebp"
【发布时间】:2011-08-23 00:37:27
【问题描述】:

enter和有什么区别

push ebp
mov  ebp, esp
sub  esp, imm

说明?有性能差异吗?如果是这样,哪个更快,为什么编译器总是使用后者?

leave

类似
mov  esp, ebp
pop  ebp

说明。

【问题讨论】:

标签: assembly x86 stack micro-optimization stack-frame


【解决方案1】:

存在性能差异,尤其是对于 enter。在现代处理器上,这解码到大约 10 到 20 µops,而三个指令序列大约是 4 到 6,具体取决于架构。详情参考Agner Fog's指令表。

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

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

【讨论】:

  • 请问您从哪里获得这些信息? leave 呢?
  • 请参阅agner.org/optimize/microarchitecture.pdf 了解处理器如何执行代码的全局概览,参阅agner.org/optimize/instruction_tables.pdf 了解详细的指令延迟。 leave 在某些架构上的性能相当,但 AFAIK 在任何情况下都不会更快。不过,它在指令缓存中消耗的内存更少
  • 如果3条指令序列比enter快,那有什么意义呢?
  • 兼容性。它自 8086 以来就一直存在,而且很可能永远存在。 loop 指令也是如此:它比 dec reg; jnz 慢得多,但它仍然存在,因为某些旧软件可能会使用它。
  • 进入/离开不在 8086/8 中。我相信它们是在 80186/8 中添加的,因为那些(很少使用)芯片具有 iapx286 的所有实模式指令(有据可查的是进入/离开)。
【解决方案2】:

使用它们中的任何一个都没有真正的速度优势,尽管 long 方法可能会运行得更好,因为 CPU 现在更“优化”到使用更通用的更短更简单的指令(加上它允许饱和执行端口,如果你幸运的话)。

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

Intel 说明手册(准确地说是第 2A 卷)将在说明中提供更多细节,Dr Agner Fogs Optimization manuals

【讨论】:

    【解决方案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

    【讨论】:

    • The "; enter n, 0 ;14 cycles on the 486" 示例缺少mov bp, sp 行。而enterleave 出现在 186 上,而不是 286 上。
    【解决方案4】:

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

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

    leave 在现代 Intel CPU 上只有 3 uop(在某些 AMD 上是 2)。 (https://agner.org/optimize/https://uops.info/)。

    mov / pop 总共只有 2 uops(在现代 x86 上,“堆栈引擎”跟踪对 ESP/RSP 的更新)。所以leave 只是比单独做事多一个uop。我在 Skylake 上对此进行了测试,将循环中的 call/ret 与设置传统帧指针并使用 mov/popleave 拆除其堆栈帧的函数进行了比较。当您使用 leave 时,perf 计数器比 mov/pop 多显示一个前端 uop。 (我进行了自己的测试,以防其他测量方法在他们的休假测量中计算堆栈同步 uop,但在实际功能控件中使用它。)

    较旧的 CPU 在保持 mov / pop 分离时可能会受益更多的可能原因:

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

    • 一些 Windows 调用约定是 callee-pops 堆栈参数,使用 ret n。 (例如ret 8 在弹出返回地址后执行 ESP/RSP += 8)。这是一条多指令指令,不像现代 x86 上的 ret 附近的普通指令。所以上面的原因是双重的:leave 和ret 12 不能在同一个循环中解码

    • 这些原因也适用于传统解码以构建 uop-cache 条目。

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

    对于现代 CPUleave 在 uop 缓存中占用 1 个额外的 uop。并且所有 3 个都必须在 uop 缓存的同一行中,这可能导致仅部分填充前一行。因此,更大的 x86 代码大小可以实际上改进了对 uop 缓存的打包。与否,取决于事情的排列方式。

    每个函数节省 2 个字节(或 3 个在 64 位模式下)可能值得,也可能不值得 1 个额外的 uop。

    GCC 偏爱 leave,clang 和 MSVC 偏爱 mov/pop(即使以牺牲速度为代价进行 clang -Oz 代码大小优化,例如使用 push 1 / pop rax(3 字节)之类的东西而不是5 字节mov eax,1)。

    ICC 支持 mov/pop,但 -Os 将使用 leavehttps://godbolt.org/z/95EnP3G1f

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-31
      • 2011-07-25
      • 2012-01-11
      • 2013-02-07
      相关资源
      最近更新 更多