【问题标题】:x86 - instruction interleaving to avoid cpu stallx86 - 指令交错以避免 CPU 停顿
【发布时间】:2016-06-19 10:47:53
【问题描述】:

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    $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 将等待pxor xmm1 返回,然后才能在下一条指令中重用它。添加指令只会填充 cpu 解码器。无论如何,cpu 必须等待。那么,为什么不让它单独听 1 条指令呢?

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

问题

这是一个错误、遗留垃圾(当 cpu 无法重新排序时)还是.. 其他?

谢谢

编辑:

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

【问题讨论】:

  • pxor xmm1, xmm1 本质上是一条 0 延迟指令
  • 你是对的,那是最糟糕的! (即使不是什么大不了的事)
  • 好吧,重新排序看起来确实没什么用。但无害
  • 同意,谢谢你的意见哈罗德
  • 如果你这么关心微优化,你为什么要使用printf

标签: gcc assembly compiler-optimization


【解决方案1】:

The x86-64 ABI 要求调用可变参数函数(如printf)设置%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 管道的详细信息,然后略读此答案。另请参阅 标签 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 产生额外的影响。

这种短视的设计选择让架构不得不在额外的依赖破坏指令或错误依赖之间做出选择。错误的 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)上的 2 uop 指令。这很奇怪,因为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 之后,它也可以在同一个周期内解码(因为 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 的 3 个微指令中的 2 个) / (最后一个 call 微指令)

所以 cvtsi2sd 在从atof 返回后的第一个周期仍然发出问题,这意味着它可以立即开始执行。即使在pxor 采用执行单元的Core2 上,来自cvtsi2sd 的2 个微指令中的第一个也可能在与pxor 相同的周期内执行。可能只有第二个微指令依赖于 dst 寄存器。

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

【讨论】:

  • 谢谢彼得,我已经阅读了这些资源。我附近还有 agner 的圣经 :) 我的问题是指令“movl $1, %eax”的存在。看起来没什么用,即使花不了多少钱,也是一些无用的工作。不是吗?
  • @Larry:叹息,我希望你的问题更清楚,你认为根本不需要指令,而不是关于重新排序的东西。更新了为什么需要它。尤其是你在 cmets 和 harold 的谈话让我觉得你只是在问订单。现在我知道你在问什么,这很清楚,但在此之前听起来你认为重新排序是无用的。
  • 另外,如果您熟悉 Agner Fog 的 microarch PDF,为什么您认为 Core2 会停止运行此代码?就像我指出的那样,printf 不会立即使用它的xmm0,所以它不在关键路径上。绝对没有什么会阻碍整个管道。 (乱序执行的全部意义在于让执行单元保持忙碌并隐藏短并行依赖链的延迟。)让一条指令比其他指令晚退休不是停顿。跨度>
  • 你说得对,我的表情很糟糕。实际上,它不会停止,因为它是在不同的端口中解码的;但这条指令没用,对吧?
  • @Larry:gcc 不会发出无用的指令,除了 NOP 用于填充对齐。请参阅我添加了新的前几段的编辑。
猜你喜欢
  • 2010-11-11
  • 2023-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-10
  • 2023-03-23
  • 2013-08-09
相关资源
最近更新 更多