【问题标题】:Why does VC++ 2010 often use ebx as a "zero register"?为什么 VC++ 2010 经常使用 ebx 作为“零寄存器”?
【发布时间】:2017-05-15 13:11:08
【问题描述】:

昨天我正在查看一些由 VC++ 2010 生成的 32 位代码(很可能;不知道具体的选项,抱歉),我对一个奇怪的反复出现的细节很感兴趣:在许多函数中,它归零了 @987654322 @ 在序言中,它总是像“零寄存器”一样使用它(想想 MIPS 上的 $zero)。特别是,它经常:

  • 用它来清零内存;这并不罕见,因为 mov mem,imm 的编码比 mov mem,reg 大 1 到 4 个字节(即使为 0,也必须对完整的立即值大小进行编码),但通常 (gcc) 必要的寄存器被清零“按需”,并保留用于其他更有用的目的;
  • 用它来与零进行比较 - 如cmp reg,ebx。这是让我感到非常不寻常的地方,因为它应该与test reg,reg 完全相同,但增加了对额外寄存器的依赖。现在,请记住,这发生在非叶函数中,ebx 经常被(被调用者)推入和推出堆栈,所以我不相信这种依赖关系总是完全免费的。此外,它以完全相同的方式使用了test reg,reg (test/cmp => jg)。

最重要的是,“经典”x86 上的寄存器是一种稀缺资源,如果您开始不得不溢出寄存器,那么您会无缘无故地浪费大量时间;为什么要在所有函数中浪费一个只是为了在其中保留一个零? (不过,仔细想想,我不记得在使用这种“零寄存器”模式的函数中看到太多寄存器溢出)。

那么:我错过了什么?这是 2010 年特别有趣的编译器故障还是一些令人难以置信的智能优化?

摘录如下:

    ; standard prologue: ebp/esp, SEH, overflow protection, ... then:
    xor     ebx, ebx
    mov     [ebp+4], ebx        ; zero out some locals
    mov     [ebp], ebx
    call    function_1
    xor     ecx, ecx            ; ebx _not_ used to zero registers
    cmp     eax, ebx            ; ... but used for compares?! why not test eax,eax?
    setnz   cl                  ; what? it goes through cl to check if eax is not zero?
    cmp     ecx, ebx            ; still, why not test ecx,ecx?
    jnz     function_body
    push    123456
    call    throw_something
function_body:
    mov     edx, [eax]
    mov     ecx, eax            ; it's not like it was interested in ecx anyway...
    mov     eax, [edx+0Ch]
    call    eax                 ; virtual method call; ebx is preserved but possibly pushed/popped
    lea     esi, [eax+10h]
    mov     [ebp+0Ch], esi
    mov     eax, [ebp+10h]
    mov     ecx, [eax-0Ch]
    xor     edi, edi            ; ugain, registers are zeroed as usual
    mov     byte ptr [ebp+4], 1
    mov     [ebp+8], ecx
    cmp     ecx, ebx            ; why not test ecx,ecx?
    jg      somewhere

label1:
    lea     eax, [esi-10h]
    mov     byte ptr [ebp+4], bl    ; ok, uses bl to write a zero to memory
    lea     ecx, [eax+0Ch]
    or      edx, 0FFFFFFFFh
    lock xadd [ecx], edx
    dec     edx
    test    edx, edx            ; now it's using the regular test reg,reg!
    jg      somewhere_else

注意:这个问题的早期版本说它使用mov reg,ebx 而不是xor ebx,ebx;这只是我没有正确记住东西。对不起,如果有人想太多试图理解这一点。

【问题讨论】:

  • 不等了,可能昨天太累了,完全不像我上面说的那样。我现在再次检查,虽然它确实将整个函数的 ebx 保持为零(这让我觉得很不寻常),但它使用它来清零 memory,其中编码为mov 更紧凑(与使用立即零相比),对于cmp,即使是针对寄存器 - 这再次让我觉得不寻常,因为cmp reg,ebx(与ebx==0)应该相同就像普通的test reg,reg。我会相应地更新问题。
  • xD,应该点击展开在输入我的答案时弹出的评论:P 是的,test reg,reg sets flags the same as a cmp against zero(立即或注册),除了未定义 AF。性能方面,test 可以在更多情况下在更多 CPU 上进行宏融合,并避免读取死寄存器(有助于 P6,因此早在 1993 年就适用)。
  • 编译器可能正在优化大小而不是速度。
  • 正如您的问题似乎暗示的那样,它并没有始终如一地这样做 - 至少在优化的代码中没有。我已经针对大量不同的代码序列详细研究了 VS 2010 的编译器生成的目标代码,但从未注意到这种模式。因此,要么您遇到了一些优化器确实认为这是有道理的边缘情况,要么您正在查看未优化的代码。不过,总体而言,您是对的,Microsoft 的编译器通常会使用零寄存器来将内存归零,因为这样更快更小,除非在非常高的寄存器压力条件下。
  • 您能否发布一些您为查看这些模式而编译的代码示例?

标签: visual-c++ assembly x86 visual-c++-2010


【解决方案1】:

您评论为奇怪的所有内容对我来说都不是最佳选择。 test eax,eax sets all flags (except AF) the same as cmp against zero,是性能和代码大小的首选。

在 P6(PPro 到 Nehalem)上,读取长期失效的寄存器是不好的,因为它会导致寄存器读取停止。 P6 内核每个时钟只能从永久寄存器文件中读取 2 或 3 个最近未修改的架构寄存器(以获取发布阶段的操作数:ROB 保存 uops 的操作数,与 SnB 系列不同,它只保存对物理寄存器文件)。

由于这是来自 VS2010,Sandybridge 还没有发布,所以它应该对 Pentium II/III、Pentium-M、Core2 和 Nehalem 进行调整,在这些地方读取“冷”寄存器是可能的瓶颈。

IDK 如果这样的事情对整数 reg 有意义,但我对 P6 之前的 CPU 的优化知之甚少。


cmp / setz / cmp / jnz 序列看起来特别脑残。也许它来自编译器内部的固定序列,用于从某些东西产生布尔值,并且它未能将布尔值的测试优化为直接使用标志?这仍然不能解释使用ebx 作为零寄存器,这在那里也完全没用。

是否有可能其中一些来自 inline-asm,它返回了一个布尔整数(使用了一个想要寄存器为零的傻瓜)?

或者也许源代码正在比较两个未知值,并且只有在内联和常量传播之后才变成与零的比较?哪个 MSVC 没有完全优化,所以它仍然在寄存器中保持 0 作为常量而不是使用test


(其余部分是在问题包含代码之前编写的)。

听起来很奇怪,或者像 CSE / 持续提升运行失控的情况。即,将 0 视为您可能想要加载一次然后在整个函数中进行 reg-reg 复制的任何其他常量。

您对数据依赖行为的分析是正确的:从不久前归零的寄存器移动基本上会启动一个新的依赖链。


当 gcc 想要两个归零的寄存器时,它通常会对一个寄存器进行异或归零,然后使用 movmovdqa 复制到另一个。

这在 Sandybridge where xor-zeroing doesn't need an execution port 上是次优的,但在 Bulldozer 系列上可能会胜出,mov 可以在 AGU 或 ALU 上运行,但异或归零仍需要 ALU 端口。

对于向量移动,Bulldozer 明显胜出:在寄存器重命名中处理,没有执行单元。但是 XMM 或 YMM 寄存器的异或归零仍然需要 Bulldozer 系列 (or two for ymm, so always use xmm with implicit zero-extension) 上的执行端口。

不过,我不认为在整个函数期间占用寄存器是合理的,尤其是在需要额外保存/恢复的情况下。并且不适用于寄存器读取停顿的 P6 系列 CPU。

【讨论】:

  • “是否有可能其中一些来自 inline-asm,它返回了一个布尔整数(使用一个想要在寄存器中为零的傻瓜)?”我会排除内联汇编,尽管我不能确定,因为我没有原始的 C++ 源代码。第二个选项看起来更有可能,但生成的程序集确实很糟糕,超出了我对一个体面的现代编译器的预期。
  • @MatteoItalia:上次我检查时,在内联辅助函数后,MSVC 没有将向量常量负载提升出循环。它可能是相当现代的,但这不是唯一严重的错过优化问题。其他编译器在某些情况下也会失败,但是从我在小范围内看到的情况来看,MSVC 更经常地生成更糟糕的代码。是的,它看起来并不像 inline asm,因为 MSVC 样式的 inline-asm 必须将任何输入存储/重新加载到 inline-asm,因为语法不允许在寄存器 (/facepalm) 中请求输入。
猜你喜欢
  • 2010-10-31
  • 1970-01-01
  • 2019-12-13
  • 1970-01-01
  • 2020-09-14
  • 2018-01-16
  • 2017-05-25
  • 2014-10-31
  • 2017-10-14
相关资源
最近更新 更多