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

x86 - instruction interleaving to avoid cpu stall

Gcc6 - 英特尔酷睿 2 双核。 编译标志:“-march=native -O3”(-S)

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

代码

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

执行

read/convert 一个整数变量,然后 read/convert 一个双精度值并将它们相加。

问题

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

我已经在上面的代码部分显示了违规指令。 对我来说,使用 cpu 重新排序和不同的执行上下文,这个交错指令是无用的。

我的理由是:无论如何我们停顿的可能性非常高,cpu 将等待 pxor xmm1 到 return,然后才能在下一条指令中重用它。添加一条指令只会白白填充 cpu 解码器。 cpu 无论如何都要等待。那么,为什么不单独使用 1 条指令呢?

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

问题

这是错误、遗留垃圾(当 cpu 无法重新订购时)还是..其他?

谢谢

编辑:

我承认我的问题不清楚:是否可以在不影响性能的情况下安全删除此指令?

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

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


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

IIRC,gcc 没有为 x86 做太多指令调度,因为它假设乱序执行。我尝试 google 那个,但没有找到我记得读过的来自 gcc 开发人员的引述(可能在 gcc 错误报告评论中)。


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


To me, with cpu reordering, and different execution context, this interleaved instruction is useless.

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

我没有花时间将其编辑成一个简短的答案,所以您可能更喜欢只需阅读 Agner Fog's microarch pdf for details of the Core2 pipeline, and skim this answer. See also other links from the 标签 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,使它错误地依赖于向量寄存器的先前值。请注意向量寄存器的上半部分如何保持其旧值。 Intel 可能是这样设计的,因为最初的 SSE cvtsi2ss 最初是在 Pentium III 上实现的,其中 128b 向量被作为两半处理。将寄存器的其余部分(包括上半部分)归零而不是合并可能会在 PIII 上花费额外的微指令。

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

在 Intel SnB 系列 CPU 上,,因此 uop 永远不需要在执行端口上执行;它一发布到ROB就已经完成了。这适用于整数和向量寄存器。

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

movl    , %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)上的 2 uop 指令。这很奇怪,因为 cvtsi2ss 是 1 uop。 (它们在 SnB 中都是 2 微指令;大概是一个微指令在整数和向量之间移动数据,另一个用于转换)。

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

不能先于pxorcall / pxor / cvtsi2sd 意味着pxor 会自行解码那个循环。解码将从call之后的指令开始,在被调用函数中的ret被解码后(并且return地址预测器预测调用后跳转回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 之后,它也可以在同一周期内解码(因为 pre-SnB 解码器最多可以处理 4-1-1-1),但它不能尽快发出。

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

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

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

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

(pxor/mov imm/cvtsi2sd), (lea / addsd / call 的 2 个微指令) /(最后一个 call uop)

所以cvtsi2sd在从atofreturning之后的第一个周期中仍然发出,这意味着它可以立即开始执行。即使在 pxor 占用一个执行单元的 Core2 上,来自 cvtsi2sd 的 2 微指令中的第一个也可能在与 pxor 相同的周期中执行。可能只有第二个 uop 对 dst 寄存器具有输入依赖性。

(mov imm / pxor / cvtsi2sd) 是等价的,解码较慢的 (pxor / cvtsi2sd / mov imm), 或者让 leamov imm.

之前执行