昨天,我看了一些由VC 2010生成的32位代码(很可能;对不起,我不知道具体的选项),我被一个奇怪的反复出现的细节所吸引:在许多函数中,它在序言中将ebx归零,并且它总是像“零寄存器”一样使用它(想想MIPS上的零)。特别是,它经常:
mov mem, imm
的编码比mov mem, reg
大1到4个字节(即使为0也必须编码完整的即时值大小),但通常(gcc)必要的寄存器会“按需”清零,并保留以用于更有用的目的;cmp reg, ebx
。这让我觉得很不寻常,因为它应该与test reg, reg
完全相同,但在额外的寄存器中添加了依赖项。现在,请记住,这发生在非叶函数中,ebx
经常被(被调用者)推送到堆栈上和堆栈外,所以我不相信这个依赖项总是完全免费的。此外,它还以完全相同的方式使用test reg, reg
(test
/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;这只是我没有正确地记住东西。对不起,如果有人想得太多,试图理解这一点。
你评论为奇数的一切在我看来都是次优的。测试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更快,但我只是想确定一下。这可能是大小和速度之间的权衡,或者是双赢(当然代码会更加不透明)。
我正在尝试实现一个简单的解决方案来解决哲学家进餐问题(有五位哲学家),我的解决方案基于以下逻辑: 每个哲学家首先思考的时间不到三秒钟 然后,如果右边有筷子,哲学家会拿走它,如果左边也有筷子,哲学家也会拿走它,开始吃不到三秒钟 然后哲学家会放下筷子,让其他哲学家也能使用 为了避免循环等待,对于最后一位哲学家,我将首先选择左边的筷子,然后选择右边的筷子,并继续相同的过程 以下是我基于此逻辑实现的代码: