C++ Clang为7次比较生成的代码比为8次比较生成的代码更差

C++ Clang为7次比较生成的代码比为8次比较生成的代码更差,c++,assembly,x86,clang,compiler-optimization,C++,Assembly,X86,Clang,Compiler Optimization,我对clang将许多小整数的==比较转换为一条大SIMD指令的能力很感兴趣,但后来我注意到了一些奇怪的事情。 与我进行8次比较时的代码相比,当我进行7次比较时,Clang生成了“更差”的代码(在我的业余评估中) bool f1(short x){ return (x==-1) | (x == 150) | (x==5) | (x==64) | (x==15) | (x==223) | (x==42) | (x==47)

我对clang将许多小整数的==比较转换为一条大SIMD指令的能力很感兴趣,但后来我注意到了一些奇怪的事情。 与我进行8次比较时的代码相比,当我进行7次比较时,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);
}
我的问题是这是一个小的性能错误,或者说clang有一个很好的理由不想引入一个虚拟比较(即,假设有一个额外的比较与7个值之一),并在代码中使用一个常量来实现它

导栓连杆:

vs


quickbench似乎不起作用,因为IDK如何为其提供-mavx2标志。(编者按:只需将UOP计算为前端成本,就可以看出这对吞吐量和延迟来说显然更糟。)

看起来clang的优化器没有考虑复制元素以使其达到SIMD方便的比较次数。但你是对的,这比做额外的标量功要好显然是遗漏了一个优化,应该报告为clang/LLVM优化器错误


f1()
的asm明显优于
f2()
vpacksswb xmm
与主流Intel和AMD CPU上的
vpmovsxwd xmm
具有相同的成本,就像其他单uop洗牌一样。如果任何东西
vpmovsx
->
vmovmskps
都可能在integer和FP域1之间有旁路延迟


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

您不需要AVX2,但在一条指令中广播单词而不使用
pshufb
控制向量确实可以提高效率。clang为
-march=nehalem


当然,这两个版本都是次优的。在movemask之前,不需要无序移动来压缩比较结果

代替
test al,al
,可以选择要使用
test sil,0b00001010
检查的位,例如,检查位1和3,但忽略其他位置的非零位

pcmpeqw
将字元素中的两个字节设置为相同的,因此可以
pmovmkb
得到一个包含成对位的整数

使用字节寄存器而不是dword寄存器也没有任何好处:
测试sil,sil
应避免REX前缀,并使用
测试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
我们执行dword广播并忽略每个32位元素的高16位中的比较结果。使用掩码进行
测试
比任何额外的指令都要便宜

如果没有AVX2,带有pshufd的SIMD dword广播比需要文字广播便宜

另一种选择是使用
0x00010001
imul
将一个字广播到32位寄存器中,但它有3个周期的延迟,因此它可能比
punpcklwd
->
pshufd


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

除非你有实际的数字,你不能说它是否更差。通过基准测试程序运行代码,看看它是否真的较慢。然后你有一个更好的问题要继续。你为什么在这里使用按位OR(
)而不是逻辑OR(
|
)?@NathanOliver:你看过asm了吗?
f2
的代码基本上包括
f1
的内容,以及更多的标量工作。(
vpacksswb-xmm
与主流Intel和AMD CPU上的
vpmovsxwd-xmm
成本相同,就像其他单uop洗牌一样)。显然,这是一个遗漏的优化,而不是仅仅重复其中一个数字,使其达到向量宽度。这应该被报告为clang/LLVM优化器错误。()非常有趣的代码生成!我想它可以通过简单地复制常量列表中的一个现有常量,为
f2
生成与
f1
相同的代码。但是,他们将其简化为仅使用4个并行比较,并串行执行其他比较。附加问题:如果扩展
f2
以进行额外比较:
|(x==VALUE)
,会发生什么?对于
VALUE==41
,clang仍然会生成次优代码(它不像
f1
)。对于值==10、34、40、43、46、58、106、170、298或554,它会生成另一种次优代码。我认为clang的这一部分正在开发中,因为clang-8发出了不同的代码。
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
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
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