为什么VC ++ 2010经常使用ebx作为"零寄存器"?

时间:2016-12-31 15:39:23

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

昨天我正在查看VC ++ 2010生成的一些32位代码(最有可能;不知道具体的选项,对不起)我被一个奇怪的反复出现的细节所吸引:在许多功能中,它归零了在序言中ebx,它总是像一个"零寄存器" (想想MIPS上的$zero)。特别是,经常:

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

最重要的是,注册" classic" x86是一种稀缺资源,如果你开始不得不泄漏寄存器,你会浪费很多时间没有充分的理由;为什么要浪费一个通过所有的功能只是为了保持零? (但是,考虑到这一点,我不记得在使用这种"零注册"模式的函数中看到很多寄存器溢出。

所以:我错过了什么?它是一个编译器blooper还是一些令人难以置信的智能优化,在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;这只是我没有正确记住的东西。对不起,如果有人想太多想到了解。

1 个答案:

答案 0 :(得分:5)

你评论的所有东西都看起来很奇怪。 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如果这样的事情对整数寄存器有意义,但我不太了解优于P6之前的CPU。

cmp / setz / cmp / jnz序列看起来特别是脑死亡。也许它来自一个编译器内部的固定序列,用于从某些东西产生一个布尔值,它无法优化布尔值的测试,直接回到直接使用标志?这仍然没有解释使用ebx作为零寄存器,这在那里也完全没用。

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

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

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

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

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

当gcc想要两个归零寄存器时,它通常为xor-zeroes,然后使用movmovdqa复制到另一个。

这在Sandybridge where xor-zeroing doesn't need an execution port上是次优的,但可能在Bulldozer家族中获胜,其中mov可以在AGU或ALU上运行,但xor-zeroing仍需要一个ALU端口。

对于向量移动,它在Bulldozer上是一个明显的胜利:在寄存器重命名处理而没有执行单元。但XMM或YMM寄存器的xor-zeroing仍然需要Bulldozer-family(or two for ymm, so always use xmm with implicit zero-extension)上的执行端口。

尽管如此,我认为这并不能证明在整个函数的持续时间内绑定一个寄存器,尤其是如果它需要额外的保存/恢复。而不是P6系列CPU,其中寄存器读取停顿是一件事。