让 g++ 使用 SHLD/SHRD 指令

Making g++ use SHLD/SHRD instructions

考虑以下代码:

#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    , %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。

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

Or are there any gcc -mtune or other options that would encourage gcc to tune better for modern Intel CPUs?

是的,你可以让 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 序列的倒数吞吐量更快。

所以这取决于 post BMI 处理器 gcc 生成更好的代码,pre-BMI clang 获胜。
SHRD 在不同处理器上的时间差异很大,我可以理解为什么 gcc 不太喜欢它。
即使使用 -Os(优化大小)gcc 仍然不会 select SHRD

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