【问题标题】:Making g++ use SHLD/SHRD instructions使 g++ 使用 SHLD/SHRD 指令
【发布时间】:2016-09-01 22:42:56
【问题描述】:

考虑以下代码:

#include <limits>
#include <cstdint>

using T = uint32_t; // or uint64_t

T shift(T x, T y, T n)
{
    return (x >> n) | (y << (std::numeric_limits<T>::digits - n));
}

根据godbolt,clang 3.8.1为-O1、-O2、-O3生成如下汇编代码:

shift(unsigned int, unsigned int, unsigned int):
        movb    %dl, %cl
        shrdl   %cl, %esi, %edi
        movl    %edi, %eax
        retq

虽然 gcc 6.2(即使使用 -mtune=haswell)生成:

shift(unsigned int, unsigned int, unsigned int):
    movl    $32, %ecx
    subl    %edx, %ecx
    sall    %cl, %esi
    movl    %edx, %ecx
    shrl    %cl, %edi
    movl    %esi, %eax
    orl     %edi, %eax
    ret

这似乎远没有优化,因为SHRD is very fast on Intel Sandybridge and later。是否有重写函数以方便编译器(尤其是 gcc)优化并支持使用 SHLD/SHRD 汇编指令?

或者是否有任何 gcc -mtune 或其他选项可以鼓励 gcc 更好地针对现代 Intel CPU 进行调整?

使用-march=haswell,它会发出 BMI2 shlx / shrx,但仍然不会 shrd。

【问题讨论】:

  • 其实差别很小。 shrd 需要 4 个周期才能解决。 sal 需要 2 个。我的猜测是 gcc 需要 7 个周期,而 clang 需要 5 个。(Skylake)在例如Bulldozer gcc 更快,因为sal/shr 是单循环,shrd 是 8。
  • @Johan:Haswell:SHRD 是 1uop,3c 延迟,每 1c 吞吐量一个。 SHL/SHR r,cl 是 3 uop,2c 延迟,每 2c 吞吐量一个。我忘记了是否可以在寄存器重命名时消除 clang 愚蠢的 8 位 mov,所以 clang 的代码在 SKL 上是 4c 或 3c 延迟,具有更多更好的吞吐量。
  • @Johan:哎呀,我在看shrd r,r,i,而不是shrd r,r,cl。可变计数版本仍然是 4 uop,具有 4c 延迟,并且在 BMI2 可用时不是最佳选择。
  • @Johan:我总是查看已经存在的电子表格版本(.ods 格式,OpenOffice,但如果需要,您可以轻松地将其转换为 Excel)。我的错误是我记得专门在我自己的 SnB 硬件上测试 SHRD,并且它在 SnB 上是有效的(并且 IACA 对 SnB 上的 SHRD/SHLD 是错误的)。但我记得的是即时计数版本,所以我只看到了我期望在 Agner 的电子表格中看到的内容。 ://
  • 我认为值得指出的是,n 为 0 是未定义的行为。

标签: c++ gcc assembly optimization bit-shift


【解决方案1】:

不,我看不出有办法让 gcc 使用 SHRD 指令。
您可以通过更改 -mtune and -march 选项来操作 gcc 生成的输出。

或者是否有任何 gcc -mtune 或其他选项可以鼓励 gcc 为现代 Intel CPU 进行更好的调优?

是的,你可以让 gcc 生成BMI2 code:

例如:X86-64 GCC6.2 -O3 -march=znver1 //AMD Zen
生成:(Haswell 计时)。

    code            critical path latency     reciprocal throughput
    ---------------------------------------------------------------
    mov     eax, 32          *                     0.25
    sub     eax, edx         1                     0.25        
    shlx    eax, esi, eax    1                     0.5
    shrx    esi, edi, edx    *                     0.5
    or      eax, esi         1                     0.25
    ret
    TOTAL:                   3                     1.75

与 clang 3.8.1 相比:

    mov    cl, dl            1                     0.25
    shrd   edi, esi, cl      4                     2
    mov    eax, edi          *                     0.25 
    ret
    TOTAL                    5                     2.25

鉴于此处的依赖链:SHRD 在 Haswell 上较慢,在 Sandybridge 上并列,在 Skylake 上较慢。
shrx 序列的倒数吞吐量更快。

所以这取决于,后 BMI 处理器 gcc 产生更好的代码,前 BMI 铿锵获胜。
SHRD 在不同的处理器上有很大不同的时间,我明白为什么 gcc 不太喜欢它。
即使使用-Os(优化大小)gcc 仍然不会选择SHRD

*) 不是时序的一部分,因为要么不在关键路径上,要么变成零延迟寄存器重命名。

【讨论】:

  • mov eax,edi(内联时消失)实际上是 Haswell 上的零延迟。而mov eax, 32 不在关键路径上,所以 BMI2 版本实际上是 4c 延迟从计数准备好,而 3c 延迟从 x 准备好。
  • shlx / shrx have a write-only destination(使用 VEX 编码),因此它们的延迟为 1c,但可以彼此并行运行(没有资源冲突,因为它们可以在 p0/p6 上运行)。当没有一个单一的依赖链时,有一个单一的“延迟”列是虚假的。所以 BMI2 版本的实际延迟是 2c 从 x 和 y 准备好, 3c 从 count 准备好。 (即如果计数不在关键路径上,则延迟仅为 2c)。对于恒定(立即)的班次计数,SHRD 可能会很有用。
  • 错过了这一点,因为我被两者中 ESI 的使用蒙蔽了双眼。很好的收获。
  • 是的,一开始也是。然后我想“等一下,他们不应该分别移动两个操作数吗?”。顺便说一句,“关键路径延迟”可能是一个很好的列标题。这些仍然是延迟数字。
猜你喜欢
  • 2017-01-09
  • 2019-08-29
  • 2015-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-18
  • 1970-01-01
  • 2011-04-27
相关资源
最近更新 更多