>用它来清零记忆;这并不罕见,因为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,其中寄存器读取停顿是一件事.