【问题标题】:Why does the VC++ compiler MOV+PUSH args instead of just PUSH them? x86为什么 VC++ 编译器 MOV+PUSH args 而不是 PUSH 它们? x86
【发布时间】:2012-10-18 22:29:23
【问题描述】:

在这个 VC++ 的反汇编中,正在进行一个函数调用。编译器在推送它们之前将本地指针移动到寄存器:

    memcpy( nodeNewLocation, pNode, sizeCurrentNode );
0041A5DA 8B 45 F8             mov         eax,dword ptr [ebp-8]  
0041A5DD 50                   push        eax  
0041A5DE 8B 4D 0C             mov         ecx,dword ptr [ebp+0Ch]  
0041A5E1 51                   push        ecx  
0041A5E2 8B 55 D4             mov         edx,dword ptr [ebp-2Ch]  
0041A5E5 52                   push        edx  
0041A5E6 E8 67 92 FF FF       call        00413852  
0041A5EB 83 C4 0C             add         esp,0Ch 

为什么不直接推送它们呢?即

push  dword ptr [ebp-8]

另外,如果您要单独推送,为什么不手动进行。换句话说,不要在上面做“push eax”,而是做

mov [esp], eax

等等。这样做的好处是,在执行 3 次 mov 之后,您可以执行一次减法来设置新的堆栈指​​针,而不是通过推送隐式减去 3 次。

更新---发布版本

这是为发布而编译的相同代码:

; 741  :    memcpy( nodeNewLocation, pNode, sizeCurrentNode );

  00087 8b 45 f8     mov     eax, DWORD PTR _sizeCurrentNode$[ebp]
  0008a 8b 7b 04     mov     edi, DWORD PTR [ebx+4]
  0008d 50       push    eax
  0008e 56       push    esi
  0008f 57       push    edi
  00090 e8 00 00 00 00   call    _memcpy
  00095 83 c4 0c     add     esp, 12            ; 0000000cH

绝对比调试版更高效,但它仍然在做 MOV/PUSH 组合。

【问题讨论】:

  • 这实际上是在发布模式下编译的吗?它看起来隐约调试
  • 它是为调试而编译的。为什么在这种情况下会有所不同?
  • 因为在调试模式下编译器不会关心这些事情。
  • 在你的最后一个例子中,通过延迟 sub 暂时使堆栈不平衡是否安全?我知道这在实模式下是个坏消息(在不合时宜的时候中断“借用”堆栈的一部分),但我不确定在保护模式下。
  • 通过解耦指令,可以减少寄存器停顿的数量。

标签: visual-c++ assembly compiler-construction x86


【解决方案1】:

这是一种优化。英特尔处理器手册第 4 卷第 12.3.3.6 节明确提到了这一点:

在 Intel Atom 微架构中,使用 PUSH/POP 指令管理堆栈空间 并且函数调用/返回之间的地址调整将比 使用 ENTER/LEAVE 替代方案。这是因为 PUSH/POP 不需要 MSROM 流和堆栈指针地址更新在 AGU 完成。 当被调用函数需要返回给调用者时,被调用者可以发出 POP 指令 恢复数据并从 EBP 恢复堆栈指针。

汇编/编译器编码规则 19。(MH 影响,M 通用性)对于英特尔 Atom处理器,喜欢PUSH/POP的寄存器形式,避免使用LEAVE;使用 LEA 调整 ESP 而不是 ADD/SUB。

手册的其余部分并不清楚原因,但它确实提到了隐式 ESP 调整可能导致 3 周期 AGU 停顿。

【讨论】:

  • 这不是真正的 Atom 问题。我正在使用 VS 10,它的编译器是在 Atom 存在之前编写的。此外,Atom 主要是用于笔记本电脑等的低功耗架构。
  • 这是一个微架构问题,Atom 只是众多实现它的芯片之一。
【解决方案2】:

我怀疑它只在调试版本中使用,或者可能在某些情况下需要通过流水线或其他考虑来保证(例如,它可以将参数放入 esi 并在调用后使用它)。我查看了一些二进制文件,MSVC 确实使用了这样的推送:

 push ebx          ; mthd
 push dword ptr [ebp+place+4]
 push dword ptr [ebp+place] ; pos
 push [ebp+filedes]   ; fh
 call __lseeki64_nolock

(来自 CRT 的代码)

至于第二个问题,寻址esp 的指令比push 长:"push eax" 是一个字节,而"mov [esp-8], eax"四个 字节。事实上,这种方法(mov 而不是push)从几个版本前(选项-maccumulate-outgoing-args)就被GCC默认使用了,它导致了notable increases in code size。据说它使代码更快,但我不相信。

【讨论】:

  • 我展示了发布版本 ASM,它具有类似的 MOV。在查看项目代码时,编译器从不进行间接推送,仅进行 MOV。我怀疑这可能是为了支持 x86 的一个非常旧的版本,比如 286 或 386 之类的。
  • @TylerDurden:每个具有 32 位寄存器的 x86 家族成员也都有“PUSH memory”指令。但它们的执行速度通常比 MOV-PUSH 组合慢,因为现代微架构并不真正喜欢具有多个内存访问的指令。
【解决方案3】:

其实我已经找到原因了。它与 Pentium MMX 上的指令流水线方式有关。有两条管道,U 和 V,允许 MMX 处理器一次处理 2 条指令IF它们是可配对的。 PUSH 不能相互配对,但它们可以与 MOV 配对。所以,如果你写:

mov eax, [indirect]
mov esi, [indirect]
push eax
push esi

然后,发生的情况是指令 #1 和 #3 配对,而 #2 和 #4 配对,因此,这四个指令有效地运行在与单个 mov/push 和单个 mov 相同的周期数/push 比两个 push [indirect] 快。第 4.3 节,第 4 页详细描述了这种确切的情况。 41,示例 4.11a 和 4.11b,Agner Fog 的微架构优化指南,可在 Internet 上广泛获取。

【讨论】:

  • 它们实际上不会在这里配对,因为它们必须在 PMMX 中相邻。如果 MSVC 仍在针对 PMMX 进行优化,我也会感到非常惊讶。
猜你喜欢
  • 2019-10-30
  • 2020-07-07
  • 2014-04-11
  • 2010-12-01
  • 2016-12-27
  • 1970-01-01
  • 2020-10-08
  • 2017-01-11
  • 1970-01-01
相关资源
最近更新 更多