为什么VC++ 2010经常用ebx作为"zero register"?
Why does VC++ 2010 often use ebx as a "zero register"?
昨天我在看一些由 VC++ 2010 生成的 32 位代码(很可能;不知道具体的选项,抱歉),我对一个奇怪的反复出现的细节很感兴趣:在许多功能,它在序言中将 ebx
归零,并且它总是像 "zero register" 一样使用它(想想 MIPS 上的 $zero
)。特别是,它经常:
- 用它来清零内存;这并不罕见,因为
mov mem,imm
的编码比 mov mem,reg
大 1 到 4 个字节(即使是 0 也必须对完整的立即值大小进行编码),但通常 (gcc) 必要的寄存器清零 "on demand",并保留用于其他更有用的目的;
- 将其用于与零进行比较 - 如
cmp reg,ebx
中所示。这让我感到非常不寻常,因为它应该与 test reg,reg
完全相同,但增加了对额外寄存器的依赖性。现在,请记住,这发生在非叶函数中,ebx
经常被(被调用者)推入和推出堆栈,所以我不相信这种依赖性总是完全免费的。此外,它 也 以完全相同的方式使用 test reg,reg
(test
/cmp
=> jg
).
最重要的是,"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 需要两个置零寄存器时,它通常将一个寄存器异或零,然后使用 mov
或 movdqa
复制到另一个。
这在 Sandybridge 上是次优的,但在推土机系列上可能获胜,其中 mov
可以在 AGU 或 ALU 上 运行,但异或归零仍然需要ALU 端口。
对于矢量移动,推土机明显胜出:在没有执行单元的寄存器重命名中处理。但是 XMM 或 YMM 寄存器的异或归零仍然需要推土机系列 () 上的执行端口。
不过,我认为这并不能证明在整个函数的持续时间内占用一个寄存器是合理的,尤其是如果它需要额外的费用 saves/restores。而不是 P6 系列 CPU,其中寄存器读取停顿是一个问题。
昨天我在看一些由 VC++ 2010 生成的 32 位代码(很可能;不知道具体的选项,抱歉),我对一个奇怪的反复出现的细节很感兴趣:在许多功能,它在序言中将 ebx
归零,并且它总是像 "zero register" 一样使用它(想想 MIPS 上的 $zero
)。特别是,它经常:
- 用它来清零内存;这并不罕见,因为
mov mem,imm
的编码比mov mem,reg
大 1 到 4 个字节(即使是 0 也必须对完整的立即值大小进行编码),但通常 (gcc) 必要的寄存器清零 "on demand",并保留用于其他更有用的目的; - 将其用于与零进行比较 - 如
cmp reg,ebx
中所示。这让我感到非常不寻常,因为它应该与test reg,reg
完全相同,但增加了对额外寄存器的依赖性。现在,请记住,这发生在非叶函数中,ebx
经常被(被调用者)推入和推出堆栈,所以我不相信这种依赖性总是完全免费的。此外,它 也 以完全相同的方式使用test reg,reg
(test
/cmp
=>jg
).
最重要的是,"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 需要两个置零寄存器时,它通常将一个寄存器异或零,然后使用 mov
或 movdqa
复制到另一个。
这在 Sandybridge mov
可以在 AGU 或 ALU 上 运行,但异或归零仍然需要ALU 端口。
对于矢量移动,推土机明显胜出:在没有执行单元的寄存器重命名中处理。但是 XMM 或 YMM 寄存器的异或归零仍然需要推土机系列 (
不过,我认为这并不能证明在整个函数的持续时间内占用一个寄存器是合理的,尤其是如果它需要额外的费用 saves/restores。而不是 P6 系列 CPU,其中寄存器读取停顿是一个问题。