visual-c – 为什么VC 2010经常使用ebx作为“零寄存器”?

昨天我正在查看VC 2010生成的一些32位代码(很可能;不知道具体的选项,对不起)我被一个奇怪的反复出现的细节所吸引:在很多功能中,它在序言中将ebx归零,它总是像“零寄存器”一样使用它(在MIPS上认为零美元).特别是,经常:

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

最重要的是,“经典”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;这只是我没有正确记住的东西.对不起,如果有人提出太多想法,试图理解这一点.

解决方法

评论的所有东西都看起来很奇怪. 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作为寄存器中的常量而不是使用测试?

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

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

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

当gcc想要两个归零寄存器时,它通常将xor-zeroes归零,然后使用mov或movdqa复制到另一个.

这在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,其中寄存器读取停顿是一件事.

相关文章

本程序的编译和运行环境如下(如果有运行方面的问题欢迎在评...
水了一学期的院选修,万万没想到期末考试还有比较硬核的编程...
补充一下,先前文章末尾给出的下载链接的完整代码含有部分C&...
思路如标题所说采用模N取余法,难点是这个除法过程如何实现。...
本篇博客有更新!!!更新后效果图如下: 文章末尾的完整代码...
刚开始学习模块化程序设计时,估计大家都被形参和实参搞迷糊...