当前位置: 首页 > 知识库问答 >
问题:

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

魏煜祺
2023-03-14

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

  • 用它来清零内存;这并不罕见,因为mov mem, imm的编码比mov mem, reg大1到4个字节(即使为0也必须编码完整的即时值大小),但通常(gcc)必要的寄存器会“按需”清零,并保留以用于更有用的目的;
  • 用它来与零进行比较——如cmp reg, ebx。这让我觉得很不寻常,因为它应该与test reg, reg完全相同,但在额外的寄存器中添加了依赖项。现在,请记住,这发生在非叶函数中,ebx经常被(被调用者)推送到堆栈上和堆栈外,所以我不相信这个依赖项总是完全免费的。此外,它还以完全相同的方式使用test reg, regtest/cmp=

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

共有1个答案

百里秋月
2023-03-14

你评论为奇数的一切在我看来都是次优的。测试eax,eax将所有标志(除AF外)设置为与cmp相同的零,并且是性能和代码大小的首选。

在P6(通过Nehalem的PPro)上,读取长死寄存器是不好的,因为它可能导致寄存器读取暂停。P6核每个时钟只能从永久寄存器文件中读取2或3个最近未修改的体系结构寄存器(以获取问题阶段的操作数:ROB保留UOP的操作数,而SnB系列上仅保留对物理寄存器文件的引用)。

由于这是来自VS2010,Sandybridge尚未发布,因此它应该非常重视Pentium II/III、Pentium-M、Core2和Nehalem的调谐,其中读取“冷”寄存器可能是一个瓶颈。

IDK,如果像这样的事情对整数正则表达式有意义的话,但我不太了解如何为P6之前的CPU进行优化。

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

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

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

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

听起来很奇怪,或者像CSE/持续吊运疯狂的案例。i、 e.将0视为您可能希望加载一次的任何其他常数,然后在整个函数中进行reg reg copy。

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

当gcc需要两个零寄存器时,它通常对一个寄存器进行异或零运算,然后使用mov或movdqa复制到另一个寄存器。

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

对于矢量移动,这在推土机上是一个明显的胜利:在没有执行单元的寄存器重命名中处理。但是XMM或YMM寄存器的异或归零仍然需要推土机系列上的执行端口(或者ymm的两个,所以始终使用具有隐式零扩展的xmm)。

尽管如此,我认为这并不能证明在整个功能期间捆绑寄存器是合理的,尤其是在需要额外保存/恢复的情况下。对于P6系列CPU来说,寄存器读取摊位是一件事。

 类似资料:
  • 根据Intel在x64中的说法,以下寄存器被称为通用寄存器(RAX、RBX、RCX、RDX、RBP、RSI、RDI、RSP和R8-R15)https://software.Intel.com/en-us/articles/indroduction-to-x64-assembly。 在下面的文章中,RBP和RSP是特殊用途寄存器(RBP指向当前堆栈帧的底部,RSP指向当前堆栈帧的顶部)。https:

  • 问题内容: 我是Go的新手,对此感到很兴奋。但是,在我广泛使用的所有语言中:Delphi,C#,C ++,Python- 列表非常重要,因为列表可以动态调整大小,而不是数组。 在Golang中,确实存在一个结构,但是我很少看到有关它的文档-无论是在Go By Example 还是我所拥有的三本Go书籍- Summerfield,Chisnal和Balbaert中,他们都花了大量时间在数组和切片上,

  • 问题内容: 我在Interface Builder中创建了一个自定义UICollectionViewCell,将其上的视图绑定到该类,然后当我想使用并将字符串设置为字符串上的标签时,tha标签的值为nil。 和子类的单元格: 问题答案: 我再打一次。 如果您使用的是情节提要,则不想调用此功能 。它将覆盖情节提要中的内容。 如果您仍然有问题请检查是否是 相同 的 ,并 在。

  • 问题内容: 有谁知道为什么下面不等于0? 要么: 当我将其输入python时,它的值为1.22e-16。 问题答案: 该数字不能完全表示为浮点数。所以,不给你,它给你。 而其实类似。 那么,您如何处理呢? 您必须计算出或至少猜测出适当的绝对和/或相对误差范围,然后编写而不是: (这也意味着你要组织你的计算,使相对误差相对较大,而不是在你的情况,因为是恒定的,这是微不足道的,只是做了落后的。) Nu

  • 我知道JE和JZ指令是相同的,而且使用OR给出了一个字节的大小改进。然而,我也关心代码速度。似乎逻辑运算符会比SUB或CMP更快,但我只是想确定一下。这可能是大小和速度之间的权衡,或者是双赢(当然代码会更加不透明)。

  • 我正在尝试实现一个简单的解决方案来解决哲学家进餐问题(有五位哲学家),我的解决方案基于以下逻辑: 每个哲学家首先思考的时间不到三秒钟 然后,如果右边有筷子,哲学家会拿走它,如果左边也有筷子,哲学家也会拿走它,开始吃不到三秒钟 然后哲学家会放下筷子,让其他哲学家也能使用 为了避免循环等待,对于最后一位哲学家,我将首先选择左边的筷子,然后选择右边的筷子,并继续相同的过程 以下是我基于此逻辑实现的代码: