Clang生成的7次比较比8次比较糟糕的代码

NoS*_*tAl 5 c++ x86 assembly clang compiler-optimization

clang能够将许多小整数的==比较转换为一条大SIMD指令,这让我很感兴趣,但是后来我注意到了一些奇怪的事情。当我进行7次比较时,Clang生成了“更差”的代码(在我的业余评估中),而当我进行8次比较时,Clang生成了该代码。

bool f1(short x){
    return (x==-1) | (x == 150) |
           (x==5) | (x==64) | 
           (x==15) | (x==223) | 
           (x==42) | (x==47);
}

bool f2(short x){
    return (x==-1) | (x == 150) |
           (x==5) | (x==64) | 
           (x==15) | (x==223) | 
           (x==42);
}
Run Code Online (Sandbox Code Playgroud)

我的问题是这是一个很小的性能错误,或者clang有一个很好的理由不想引入虚拟比较(即假装与7个值之一进行额外的比较),并在代码中使用一个更多的常量来实现它。

Godbolt链接在这里

# clang(trunk) -O2 -march=haswell
f1(short):
    vmovd   xmm0, edi
    vpbroadcastw    xmm0, xmm0             # set1(x)
    vpcmpeqw        xmm0, xmm0, xmmword ptr [rip + .LCPI0_0]  # 16 bytes = 8 shorts
    vpacksswb       xmm0, xmm0, xmm0
    vpmovmskb       eax, xmm0
    test    al, al
    setne   al           # booleanize the parallel-compare bitmask
    ret
Run Code Online (Sandbox Code Playgroud)

f2(short):
    cmp     di, -1
    sete    r8b
    cmp     edi, 150
    sete    dl
    cmp     di, 5             # scalar checks of 3 conditions
    vmovd   xmm0, edi
    vpbroadcastw    xmm0, xmm0
    vpcmpeqw        xmm0, xmm0, xmmword ptr [rip + .LCPI1_0]  # low 8 bytes = 4 shorts
    sete    al
    vpmovsxwd       xmm0, xmm0
    vmovmskps       esi, xmm0
    test    sil, sil
    setne   cl                # SIMD check of the other 4
    or      al, r8b
    or      al, dl
    or      al, cl            # and combine.
    ret
Run Code Online (Sandbox Code Playgroud)

quickbench似乎不起作用,因为IDK如何为其提供-mavx2标志。(编者注:简单地将uops计入前端成本表明,这显然会降低吞吐量。而且还会带来延迟。)

Pet*_*des 5

看起来 clang 的优化器没有考虑复制元素以使其达到 SIMD 方便的比较次数。但你是对的,这比做额外的标量工作要好。 显然,错过了优化,应该将其报告为 clang/LLVM 优化器错误。https://bugs.llvm.org/


asm forf1()显然比f2():vpacksswb xmm具有与主流 Intel 和 AMD CPU 相同的成本vpmovsxwd xmm,就像其他单 uop shuffle 一样。如果有的话vpmovsx->vmovmskps可能在整数域和 FP 域之间有旁路延迟1


脚注 1:采用 AVX2(Sandybridge 系列)的主流 Intel CPU 上可能没有额外的旁路延迟;FP 操作之间的整数洗牌通常很好,IIRC。(https://agner.org/optimize/)。但对于 Nehalem 上的 SSE4.1 版本,是的,可能会有整数版本没有的额外惩罚。

您不需要 AVX2,但在没有pshufb控制向量的情况下在一条指令中进行字广播确实会提高效率。并且 clang 选择pshuflw-> pshufdfor-march=nehalem


当然,这两个版本都不是最优的。在 movemask 之前不需要进行 shuffle 来压缩比较结果。

可以test al, al选择要检查的位test sil, 0b00001010,例如检查位 1 和 3,但忽略其他位置的非零位,而不是 。

pcmpeqw将字元素内的两个字节设置为相同,因此该结果很好pmovmskb并获得具有位对的整数。

使用字节寄存器而不是双字寄存器的好处也为零:test sil,sil应该避免 REX 前缀并使用test esi,esi.

因此,即使重复其中一个条件,f2()也可能是:

f2:
    vmovd           xmm0, edi
    vpbroadcastw    xmm0, xmm0             # set1(x)
    vpcmpeqw        xmm0, xmm0, xmmword ptr [rip + .LCPI0_0]
    vpmovmskb       eax, xmm0
    test    eax, 0b011111111111111    # (1<<15) - 1 = low 14 bits set
    setne   al
    ret
Run Code Online (Sandbox Code Playgroud)

test将根据结果的低 14 位设置 ZF pmovmksb,因为高位在 TEST 掩码中被清除。TEST = AND 不写入其输出。对于选择比较掩码的部分通常很有用。

但由于我们首先需要在内存中使用一个 16 字节常量,所以我们应该复制其中一个元素以将其填充到 8 个元素。然后我们就可以test eax,eax像正常人一样使用了。压缩掩码以适应 8 位AL完全是浪费时间和代码大小。 test r32, r32与 SIL、DIL 或 BPL一样快test r8,r8,并且不需要 REX 前缀。

有趣的事实:AVX512VL 可以让我们结合使用vpbroadcastw xmm0, edimovd广播。


或者只比较 4 个元素,而不是对 进行额外的改组movmskps,我们这里只需要 SSE2。使用口罩确实很有用。

test_4_possibilities_SSE2:
    movd            xmm0, edi
    pshufd          xmm0, xmm0, 0             # set1_epi32(x)
    pcmpeqw         xmm0, [const]             # == set_epi32(a, b, c, d)
    pmovmskb        eax, xmm0
    test    eax, 0b0001000100010001     # the low bit of each group of 4
    setne   al
    ret
Run Code Online (Sandbox Code Playgroud)

我们进行双字广播并忽略每个 32 位元素的高 16 位中的比较结果。使用掩码可以test让我们比任何额外的指令更便宜地做到这一点。

如果没有 AVX2,SIMD 双字广播pshufd比需要字广播便宜。

另一种选择是imul0x00010001一个字广播到 32 位寄存器中,但这有 3 个周期延迟,因此可能比punpcklwd->更糟糕pshufd

不过,在循环内,值得为pshufb(SSSE3) 加载一个控制向量,而不是使用 2 个 shuffle 或一个 imul。