x86 - 指令交错以避免cpu停顿

时间:2016-06-19 10:47:54

标签: gcc assembly compiler-optimization

Gcc6 - intel core 2 duo。 编译标志:" -march = native -O3" (-S)

我正在编译一个简单的程序并要求汇编输出:

代码

movq    8(%rsi), %rdi
call    _atoi
movq    16(%rbp), %rdi
movl    %eax, %ebx
call    _atof
pxor    %xmm1, %xmm1
movl    $1, %eax <- this instruction is my problem
cvtsi2sd    %ebx, %xmm1
leaq    LC0(%rip), %rdi
addsd   %xmm1, %xmm0
call    _printf
addq    $8, %rsp

执行

读取/转换整数变量,然后读取/转换double值并添加它们。

问题

我完全理解一个(编译器更多)必须尽可能地避免cpu停滞。

我在上面的代码部分中显示了有问题的说明。 对我来说,通过cpu重新排序和不同的执行上下文,这个交错的指令是没用的。

我的理由是:无论如何我们停止的机会非常高,并且cpu将等待pxor xmm1返回,然后才能在下一条指令中重用它。添加指令只会填充cpu解码器。无论如何,cpu还在等待。那么为什么不单独留一条指令呢?

在atof之前移动pxor似乎是不可能的,因为atof可能会使用它。

问题

这是一个bug,一个遗留垃圾(当cpu无法重新排序时)或者......其他?

由于

编辑:

我承认我的问题不明确:是否可以安全删除此说明而不会产生性能影响?

1 个答案:

答案 0 :(得分:1)

The x86-64 ABI要求调用varargs函数(如printf)set %al =在xmm寄存器中传递的浮点args的计数。在这种情况下,您传递了一个double,因此ABI需要%al = 1。 (有趣的事实:C&#39的推广规则使得无法将float传递给vararg函数。这就是为什么没有float的printf转换说明符,只有double。 )

mov $1, %eax避免了对eax其余部分的错误依赖,(与mov $1, %al相比),因此gcc更喜欢在其上花费额外的指令字节,即使它正在调整Core2(重命名部分寄存器)。

之前的答案,在澄清之前,问题是为什么mov全部完成,而不是关于它的排序。

IIRC,gcc没有为x86做很多指令调度,因为它假设无序执行。我试着谷歌那个,但是没有找到我似乎记得读过的gcc开发者的引用(可能是在gcc bug报告评论中)。

无论如何,它看起来不错,除非您正在调整有序Atom或P5。如果是,请使用gcc -O3 -march=atom(这意味着-mtune=atom)。但无论如何,你显然没有这样做,因为你在C2Duo上使用-march=native,这是一个4宽的无序设计,具有相当大的调度程序。

  

对我来说,通过cpu重新排序和不同的执行上下文,这个交错的指令是没用的。

我不知道你认为问题是什么,或者你认为哪种顺序会更好,所以我只是解释为什么它看起来不错。

我没有花时间将其编辑为简短的答案,所以您可能更愿意只需阅读Agner Fog's microarch pdf以获取Core2管道的详细信息,并浏览此内容回答。另请参阅标记wiki的其他链接。

...
call    _atof
   # xmm0 is probably still not ready when the following instructions issue
pxor    %xmm1, %xmm1          # no inputs, so can run any time after being issued.

gcc使用pxor因为cvtsi2sd is badly designed,使其对向量寄存器的先前值具有错误依赖性。注意向量寄存器的上半部分如何保持其旧值。英特尔可能就是这样设计的,因为原始SSE cvtsi2ss最初是在Pentium III上实现的,其中128b向量被处理为两半。将剩余的寄存器(包括上半部分)归零而不是合并可能会对PIII产生额外的影响。

这种短视的设计选择使得架构可以选择额外的依赖性依赖性指令或虚假依赖性。如果一个函数使用的寄存器恰好用于另一个函数中的非常长的FP依赖链(可能包括缓存未命中),那么false dep可能根本不重要,或者可能是一个大的减速。

在Intel SnB系列CPU上,xor-zeroing is handled at register-rename time,因此uop永远不需要在执行端口上执行;它一发布到ROB就已经完成了。对于整数和向量寄存器都是如此。

在其他CPU上,pxor将需要一个执行端口,但没有输入依赖性,所以它可以在任何时候执行,并且在它发出后有一个免费的ALU端口。

movl    $1, %eax             # no input dependencies, can execute any time.

此说明可以放在call atof之后和call printf之前的任何地方。

cvtsi2sd    %ebx, %xmm1       # no false dependency thanks to pxor.

这是关于Core2(Merom和Penryn)的2 uop指令,根据Agner Fog的表格。这很奇怪,因为cvtsi2ss是1 uop。 (它们在SnB中都是2个uop;大概是一个uop来在整数和向量之间移动数据,另一个用于转换)。

早些时候把这个insn推得很好,可能会提前一个周期,因为它是这里最长的依赖链的一部分。 (整数的东西都很简单而且很简单)。但是,printf必须在决定查看xmm0之前解析格式字符串,因此FP指令实际上并不在关键路径上。

它不能超前pxor,而call / pxor / cvtsi2sd意味着pxor会自行解码该周期。解码将从call之后的指令开始,在被调用函数中的ret被解码之后(并且返回地址预测器预测在调用之后跳回到insn)。多uop指令必须是块中的第一条指令,因此pxormov imm32解码该周期意味着更少的解码瓶颈。

leaq    LC0(%rip), %rdi        # 1 uop
addsd   %xmm1, %xmm0           # 1 uop
call    _printf                # 3 uop insn

cvtsi2sd / lea / addsd都可以在同一周期内解码,这是最佳的。如果mov imm32在cvt之后,它也可以在相同的周期内解码(因为预SnB解码器可以处理最多4-1-1-1),但它不能发布为很快。

如果解码几乎没有跟上问题,那就意味着pxor会自行发布(因为还没有解码其他指令)。然后cvtsi2sd / mov imm / lea(4 uops),然后addsd / call(4 uops)。 (addsd使用前一个问题组解码; core2在解码和发布之间有一个短队列,以帮助吸收这样的解码气泡,并使其能够在一个周期内解码多达7个uop。)

与解码瓶颈情况中的当前问题模式没有明显不同:(pxor / mov imm)/(cvtsi2sd / lea / addsd)/(call printf

如果解码不是瓶颈,我不确定Core2是否可以在与跳转后的uops相同的周期内发出retjmp。在SnB系列CPU中,无条件跳转始终结束问题组。例如3-uop循环发出ABCABCABC,而非ABCABCABCABC

假设ret问题后的说明不包括ret,我们就

pxor / mov imm / cvtsi2sd),(lea / addsd / 2 call 3个uops )/(最后call uop)

因此cvtsi2sd在从atof返回后仍然在第一个周期发出,这意味着它可以立即开始执行。即使在pxor占用执行单元的Core2上,cvtsi2sd的2个uop中的第一个也可能在与pxor相同的周期内执行。它可能只是第二个uop,它对dst寄存器有输入依赖。

mov imm / pxor / cvtsi2sd)将是等效的,因此解码速度较慢(pxor / cvtsi2sd / {{ 1}}),或在mov imm之前执行lea