昨天我正在查看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,reg
(test
/ 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
;这只是我没有正确记住的东西。对不起,如果有人想太多想到了解。
答案 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,然后使用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,其中寄存器读取停顿是一件事。