X86-指令交错,避免CPU停顿 [英] x86 - instruction interleaving to avoid cpu stall

查看:10
本文介绍了X86-指令交错,避免CPU停顿的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Gcc6-英特尔酷睿2二人组。 编译标志:"-3月=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

执行

读取/转换整数变量,然后读取/转换双精度值并将其相加。

问题

我完全理解一个人(编译器更是如此)必须尽可能避免CPU停止。

我已经在上面的代码部分中显示了违规指令。 对我来说,随着CPU的重新排序,以及不同的执行上下文,这种交错指令毫无用处。

我的理由是:无论如何我们停顿的可能性都很高,CPU将等待pxorxmm1返回,然后才能在下一条指令中重用它。添加一条指令只会徒劳地填满CPU解码器。无论如何,CPU都必须等待。那么,为什么不把它放在一边,等待一条指令呢?

pxor移到atof之前似乎是不可能的,因为atof可以使用它。

问题

是错误、遗留垃圾(当CPU无法重新排序时)还是..否则?

谢谢

编辑:

我承认我的问题不清楚:是否可以安全地删除此指令而不会影响性能?

xmm

The x86-64 ABI要求调用推荐答案函数(如printf)set%al=在xmm寄存器中传递的浮点参数的计数。在本例中,您传递的是一个double,因此ABI需要%al = 1。(有趣的事实:C的提升规则使得不可能将float传递给vararg函数。这就是为什么float没有printf转换说明符,只有double。)

mov $1, %eax避免了对eax其余部分的错误依赖(与mov $1, %al相比),因此GCC更愿意在这方面花费额外的指令字节,即使它是针对Core2(重命名部分寄存器)进行调整的。


上一个答案,在澄清问题是为什么要全部完成mov之前,而不是关于其顺序。

IIRC,GCC没有为x86做太多的指令调度,因为它假设执行是无序的。我试着在谷歌上搜索,但没有找到我似乎记得读到的GCC开发人员的话(可能是在GCC的一篇错误报告评论中)。


不管怎样,我觉得它还可以,除非您调优的是按顺序的Atom或P5。如果是,请使用gcc -O3 -march=atom(这意味着-mtune=atom)。但无论如何,您显然没有这样做,因为您在C2Duo上使用了-march=native,这是一个4宽的无序设计,具有相当大的调度器。


对我来说,由于CPU重新排序,以及不同的执行上下文,这条交错的指令毫无用处。

我不知道您认为是什么问题,也不知道您认为什么顺序会更好,所以我只解释为什么它看起来很好。

我没有花时间将此编辑为简短的答案,因此您可能更喜欢阅读Agner Fog's microarch pdf以了解Core2管道的详细信息,并略读此答案。另请参阅标签维基中的其他链接。


...
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,这给了它对向量寄存器上一个值的错误依赖。注意向量寄存器的上半部分如何保持其旧值。英特尔之所以这样设计,可能是因为最初的SSEcvtsi2ss首先在奔腾III上实现,在奔腾III中,128B向量被作为两部分处理。将寄存器的其余部分(包括上半部分)置零而不是合并,可能会在PIII上花费额外的uop。

这种短视的设计选择让体系结构在一个额外的打破依赖的指令或一个错误的依赖之间做出选择。错误的dep可能根本不重要,或者如果一个函数使用的寄存器碰巧用于另一个函数中非常长的FP依赖性链(可能包括缓存未命中),则可能会大大降低速度。

在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.

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

将该INSN放得更早会更好,可能会提前发出一个周期,因为它是这里最长的依赖链的一部分。(整型的东西都是简单而琐碎的)。但是,printf必须先分析格式字符串,然后才能决定查看xmm0,因此FP指令实际上不在关键路径上。

它不能在pxor之前,call/pxor/cvtsi2sd将意味着pxor将自己解码该循环。解码将从call之后的指令开始,在被调用函数中的ret被解码之后(并且返回地址预测器预测在调用之后跳回到INSN)。多微指令必须是块中的第一条指令,因此让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个uop),然后addsd/call(4个uop)。(addsd与前一个问题组一起解码;Core2在解码和发布之间有一个较短的队列,以帮助吸收这样的解码气泡,并使其能够在一个周期内解码多达7个uop。)

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

如果解码不是瓶颈,我不确定Core2是否可以在与跳转后的uop相同的周期内发出retjmp。在SNB系列CPU中,无条件跳转总是会结束一个问题组。例如,3-uop循环问题ABCABCABC、NOTABCABCABCABC

假设ret问题之后的说明不包括ret,我们将有

(pxor/mov imm/cvtsi2sd),(lea/addsd/2个calluop)/(最后一个calluop)

所以cvtsi2sd在从atof返回后的第一个周期中仍然发出,这意味着它可以立即开始执行。即使在Core2上,pxor采用一个执行单元,cvtsi2sd的两个uop中的第一个也可能与pxor在相同的周期内执行。可能只有第二个微操作对DST寄存器具有输入依赖关系。

(mov imm/pxor/cvtsi2sd)是等价的,解码速度较慢的(pxor/cvtsi2sd/mov imm),或者在mov imm之前执行lea

这篇关于X86-指令交错,避免CPU停顿的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆