为什么VC++ 2010经常用ebx作为"zero register"?

Why does VC++ 2010 often use ebx as a "zero register"?

昨天我在看一些由 VC++ 2010 生成的 32 位代码(很可能;不知道具体的选项,抱歉),我对一个奇怪的反复出现的细节很感兴趣:在许多功能,它在序言中将 ebx 归零,并且它总是像 "zero register" 一样使用它(想想 MIPS 上的 $zero)。特别是,它经常:

最重要的是,"classic" x86 上的寄存器是一种稀缺资源,如果您开始不得不溢出寄存器,您会无缘无故地浪费大量时间;为什么要在所有函数中浪费一个只是为了在其中保留一个零? (仍然,考虑一下,我不记得在使用这种 "zero-register" 模式的函数中看到过太多寄存器溢出)。

那么:我错过了什么?是编译器故障还是一些在 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;这只是我没有正确记住东西。对不起,如果有人花了太多心思试图理解这一点。

在我看来,您认为奇怪的所有内容都不是最佳选择。 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 的调整上投入了很多精力,其中阅读 "cold" 寄存器可能是一个瓶颈。

IDK 如果这样的事情对整数 regs 有意义,但我对优化 P6 之前的 CPU 了解不多。


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

是否有可能其中一些来自返回布尔整数的内联 asm(使用一个希望寄存器中为零的傻瓜)?

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


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

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

您对数据依赖行为的分析是正确的:从一个前一段时间清零的寄存器中移出,本质上会启动一个新的依赖链。


当 gcc 需要两个置零寄存器时,它通常将一个寄存器异或零,然后使用 movmovdqa 复制到另一个。

这在 Sandybridge 上是次优的,但在推土机系列上可能获胜,其中 mov 可以在 AGU 或 ALU 上 运行,但异或归零仍然需要ALU 端口。

对于矢量移动,推土机明显胜出:在没有执行单元的寄存器重命名中处理。但是 XMM 或 YMM 寄存器的异或归零仍然需要推土机系列 () 上的执行端口。

不过,我认为这并不能证明在整个函数的持续时间内占用一个寄存器是合理的,尤其是如果它需要额外的费用 saves/restores。而不是 P6 系列 CPU,其中寄存器读取停顿是一个问题。