【问题标题】:Is it possible to check if any of 2 sets of 3 ints is equal with less than 9 comparisons?是否可以检查两组 3 个整数中的任何一组是否等于少于 9 次比较?
【发布时间】:2016-05-28 11:28:07
【问题描述】:
int eq3(int a, int b, int c, int d, int e, int f){
    return a == d || a == e || a == f 
        || b == d || b == e || b == f 
        || c == d || c == e || c == f;
}

此函数接收 6 个整数,如果前 3 个整数中的任何一个等于最后 3 个整数中的任何一个,则返回 true。有没有类似的按位破解方法可以让它更快?

【问题讨论】:

  • 我想你写的版本会很快。您是否分析过您的程序以确定这是一个瓶颈?
  • @DietrichEpp 90% 的运行时间都花在了那个函数上
  • @chux 应该适用于所有 32 位整数。这实际上是我需要的,一个非对称版本,其中的错误结果要快得多。
  • 您最好的选择可能是编写一个 SSE2 向量比较,并将前 3 个和后 3 个视为 2 个向量。 Here 就是一个例子。
  • 您对每个输入参数的预期分布有任何想法吗?无论它们是否均等分布,或者它们是否更有可能变化(例如它们的低位),都会产生巨大的差异。

标签: c performance bit-manipulation micro-optimization


【解决方案1】:

假设您预计 false 结果的比率很高,您可以进行快速“预检查”以快速隔离此类情况:

如果设置了a 中的某个位,但未在def 中设置,则a 不能等于其中任何一个。

类似

int pre_eq3(int a, int b, int c, int d, int e, int f){
    int const mask = ~(d | e | f);
    if ((a & mask) && (b & mask) && (c & mask)) {
         return false;
    }
    return eq3(a, b, c, d, e, f);
}

可以加速它(8 次操作而不是 9 17,但如果结果实际上是true,则成本会更高)。如果mask == 0 那么这当然无济于事。


如果a & b & c 很有可能设置了一些位,则可以进一步改进:

int pre_eq3(int a, int b, int c, int d, int e, int f){
    int const mask = ~(d | e | f);
    if ((a & b & c) & mask) {
        return false;
    }
    if ((a & mask) && (b & mask) && (c & mask)) {
         return false;
    }
    return eq3(a, b, c, d, e, f);
}

现在,如果 所有 a、b 和 c 都设置了位,而 d、e 和 c 都没有设置任何位,我们很快就会退出。

【讨论】:

  • 我正在研究一个猜测更少、数据更多的答案。
【解决方案2】:

如果您想要按位版本,请查看异或。如果您对两个相同的数字进行异或运算,则答案将为 0。否则,如果一个被设置而另一个未被设置,则这些位将翻转。例如 1000 xor 0100 是 1100。

您拥有的代码可能会导致至少 1 次管道刷新,但除此之外它在性能方面还可以。

【讨论】:

  • 我也尝试过,但结果是相同数量的比较加上异或操作。虽然我认为这是一个可能的解决方案。你有例子吗?
  • 我不适合这种情况。有可能,它需要几个小时的笔和纸工作才能完成数学运算以使其正确。不幸的是,我没有这几个小时。
【解决方案3】:

我认为使用 SSE 可能值得研究。

我写任何东西已经 20 年了,而且没有基准测试,但类似:

#include <xmmintrin.h>
int cmp3(int32_t a, int32_t b, int32_t c, int32_t d, int32_t e, int32_t f){
    // returns -1 if any of a,b,c is eq to any of d,e,f
    // returns 0 if all a,b,c != d,e,f
    int32_t __attribute__ ((aligned(16))) vec1[4];
    int32_t __attribute__ ((aligned(16))) vec2[4];
    int32_t __attribute__ ((aligned(16))) vec3[4];
    int32_t __attribute__ ((aligned(16))) vec4[4];
    int32_t __attribute__ ((aligned(16))) r1[4];
    int32_t __attribute__ ((aligned(16))) r2[4];
    int32_t __attribute__ ((aligned(16))) r3[4];

    // fourth word is DNK
    vec1[0]=a;
    vec1[1]=b;
    vec1[2]=c;

    vec2[0]=vec2[1]=vec2[2]=d;
    vec3[0]=vec3[1]=vec3[2]=e;
    vec4[0]=vec4[1]=vec4[2]=f;

    __m128i v1 = _mm_load_si128((__m128i *)vec1);
    __m128i v2 = _mm_load_si128((__m128i *)vec2);
    __m128i v3 = _mm_load_si128((__m128i *)vec3);
    __m128i v4 = _mm_load_si128((__m128i *)vec4);

    // any(a,b,c) == d? 
    __m128i vcmp1 = _mm_cmpeq_epi32(v1, v2);
    // any(a,b,c) == e?
    __m128i vcmp2 = _mm_cmpeq_epi32(v1, v3);
    // any(a,b,c) == f?
    __m128i vcmp3 = _mm_cmpeq_epi32(v1, v4);

    _mm_store_si128((__m128i *)r1, vcmp1);
    _mm_store_si128((__m128i *)r2, vcmp2);
    _mm_store_si128((__m128i *)r3, vcmp3);

    // bit or the first three of each result.
    // might be better with SSE mask, but I don't remember how!
    return r1[0] | r1[1] | r1[2] |
           r2[0] | r2[1] | r2[2] |
           r3[0] | r3[1] | r3[2];
}

如果操作正确,没有分支的 SSE 应该快 4 到 8 倍。

【讨论】:

  • 为什么返回-1?
  • 这是英特尔的惯例。如果相等,则英特尔内部函数返回 0xFFFFFFF,否则返回 0x00xFFFFFFF 的值当然是 -1(如果解释为有符号整数)。如果您愿意,您可以将返回更改为unsigned。参考是HERE
  • 顺便说一句:如果您可以更改代码以调用 cmp3 并使用已加载的向量而不是将 a, b, c, d, e, f 转换为函数内的向量,这将更有效。
  • 您在正确的轨道上,但您应该使用向量 OR 来组合三个结果。是的,_mm_movemask_epi8 将是比存储到内存更好的选择。此外,这是将整数转换为向量的一种可怕方式。使用 set / set1 内在函数让编译器做出最佳选择。 (并且存储到这样的数组不是最好的选择:int->vector 然后随机播放)。此外,由于未知原因,gcc 编译您的函数非常糟糕。就像,甚至比源代码告诉它如何处理这些数组更糟糕,xD。看我的回答。
【解决方案4】:

扩展 dawg 的 SSE 比较方法,您可以使用向量 OR 组合比较结果,并将比较结果的掩码移回整数以测试 0/非零。

此外,您还可以更有效地将数据放入向量中(尽管将许多单独的整数放入向量中,因为它们一开始就存在于寄存器中,而不是放在内存中)。

你应该避免store-forwarding stalls 是因为做三个小商店和一个大负载。

///// UNTESTED ////////
#include <immintrin.h>
int eq3(int a, int b, int c, int d, int e, int f){

    // Use _mm_set to let the compiler worry about getting integers into vectors
    // Use -mtune=intel or gcc will make bad code, though :(
    __m128i abcc = _mm_set_epi32(0,c,b,a);  // args go from high to low position in the vector
    // masking off the high bits of the result-mask to avoid false positives
    // is cheaper than repeating c (to do the same compare twice)

    __m128i dddd = _mm_set1_epi32(d);
    __m128i eeee = _mm_set1_epi32(e);

    dddd = _mm_cmpeq_epi32(dddd, abcc);
    eeee = _mm_cmpeq_epi32(eeee, abcc);  // per element: 0(unequal) or -1(equal)
    __m128i combined = _mm_or_si128(dddd, eeee);

    __m128i ffff = _mm_set1_epi32(f);
    ffff = _mm_cmpeq_epi32(ffff, abcc);
    combined = _mm_or_si128(combined, ffff);

    // results of all the compares are ORed together.  All zero only if there were no hits
    unsigned equal_mask = _mm_movemask_epi8(combined);
    equal_mask &= 0x0fff;  // the high 32b element could have false positives
    return equal_mask;
    // return !!equal_mask if you want to force it to 0 or 1
    // the mask tells you whether it was a, b, or c that had a hit

    // movmskps would return a mask of just 4 bits, one for each 32b element, but might have a bypass delay on Nehalem.
    // actually, pmovmskb apparently runs in the float domain on Nehalem anyway, according to Agner Fog's table >.<
}

这编译为非常好的 asm,clang 和 gcc 非常相似,但 clang's -fverbose-asm puts nice comments on the shuffles。只有 19 条指令,包括 ret,具有来自独立依赖链的相当数量的并行性。使用-msse4.1-mavx,它可以节省另外几条指令。 (但可能不会跑得更快)

使用 clang,dawg 的版本大约是两倍大小。使用 gcc,会发生一些不好的事情,而且很可怕(超过 80 条指令。看起来像 gcc 优化错误,因为它看起来比直接翻译源代码更糟糕)。即使是 clang 的版本也花费了很长时间将数据输入/输出向量 reg,以至于只进行无分支比较和 OR 真值一起可能会更快。

这编译成不错的代码:

// 8bit variable doesn't help gcc avoid partial-register stalls even with -mtune=core2 :/
int eq3_scalar(int a, int b, int c, int d, int e, int f){
    char retval = (a == d) | (a == e) | (a == f)
         | (b == d) | (b == e) | (b == f)
         | (c == d) | (c == e) | (c == f);
    return retval;
}

尝试如何将数据从调用者获取到向量寄存器中。 如果三人一组来自记忆,那么概率。传递指针以便向量加载可以从它们的原始位置获取它们是最好的。在通往向量的过程中通过整数寄存器很糟糕(更高的延迟,更多的微指令),但如果你的数据已经存在于寄存器中,那么进行整数存储然后向量加载是一种损失。 gcc 是愚蠢的,并且遵循 AMD 优化指南的建议通过内存反弹,尽管 Agner Fog 说他发现即使在 AMD CPU 上也不值得。在 Intel 上肯定更糟,在 AMD 上显然更糟,甚至更糟,所以这绝对是 -mtune=generic 的错误选择。总之……


也可以进行 9 次比较中的 8 次,而只需进行两次压缩向量比较。

第 9 个可以通过整数比较来完成,并将其真值与向量结果进行 OR 运算。在某些 CPU(尤其是 AMD,可能还有 Intel Haswell 及更高版本)上,根本不将 6 个整数之一传输到向量 regs 可能是一个胜利。将三个整数无分支比较与向量混洗/比较混合在一起可以很好地交错。

可以通过对整数数据使用shufps 来设置这些向量比较(因为它可以组合来自两个源寄存器的数据)。这在大多数 CPU 上都很好,但在使用内部函数而不是实际的 asm 时需要大量烦人的转换。即使存在绕过延迟,与 punpckldq 和 pshufd 之类的东西相比,这也不是一个糟糕的权衡。

aabb    ccab
====    ====
dede    deff

c==f

与 asm 类似:

#### untested
# pretend a is in eax, and so on
movd     xmm0, eax
movd     xmm1, ebx
movd     xmm2, ecx

shl      rdx, 32
#mov     edi, edi     # zero the upper 32 of rdi if needed, or use shld instead of OR if you don't care about AMD CPUs
or       rdx, rdi     # de in an integer register.
movq     xmm3, rdx    # de, aka (d<<32)|e
# in 32bit code, use a vector shuffle of some sort to do this in a vector reg, or:
#pinsrd  xmm3, edi, 1  # SSE4.1, and 2 uops (same as movd+shuffle)
#movd    xmm4, edi    # e

movd     xmm5, esi    # f

shufps   xmm0, xmm1, 0            #  xmm0=aabb  (low dword = a; my notation is backwards from left/right vector-shift perspective)
shufps   xmm5, xmm3, 0b01000000   # xmm5 = ffde  
punpcklqdq xmm3, xmm3            # broadcast: xmm3=dede
pcmpeqd  xmm3, xmm0              # xmm3: aabb == dede

# spread these instructions out between vector instructions, if you aren't branching
xor      edx,edx
cmp      esi, ecx     # c == f
#je   .found_match    # if there's one of the 9 that's true more often, make it this one.  Branch mispredicts suck, though
sete     dl

shufps   xmm0, xmm2, 0b00001000  # xmm0 = abcc
pcmpeqd  xmm0, xmm5              # abcc == ffde

por      xmm0, xmm3
pmovmskb eax, xmm0    # will have bits set if cmpeq found any equal elements
or       eax, edx     # combine vector and scalar compares
jnz  .found_match
# or record the result instead of branching on it
setnz    dl

这也是19条指令(不包括最后的jcc / setcc),但其中一条是异或归零习语,还有其他简单的整数指令。 (更短的编码,有些可以在不能处理向量指令的 Haswell+ 上的 port6 上运行)。由于构建 abcc 的 shuffle 链,可能存在更长的 dep 链。

【讨论】:

  • 这很棒。谢谢你。
【解决方案5】:

如果您的编译器/架构支持 vector extensions(如 clang 和 gcc),您可以使用类似:

#ifdef __SSE2__
#include <immintrin.h>
#elif defined __ARM_NEON
#include <arm_neon.h>
#elif defined __ALTIVEC__
#include <altivec.h>
//#elif ... TODO more architectures
#endif

static int hastrue128(void *x){
#ifdef __SSE2__
    return _mm_movemask_epi8(*(__m128i*)x);
#elif defined __ARM_NEON
    return vaddlvq_u8(*(uint8x16_t*)x);
#elif defined __ALTIVEC__
typedef __UINT32_TYPE__ v4si __attribute__ ((__vector_size__ (16), aligned(4), __may_alias__));
    return vec_any_ne(*(v4si*)x,(v4si){0});
#else
    int *y = x;
    return y[0]|y[1]|y[2]|y[3];
#endif
}

//if inputs will always be aligned to 16 add an aligned attribute
//otherwise ensure they are at least aligned to 4
int cmp3(  int* a  ,  int* b ){
typedef __INT32_TYPE__ i32x4 __attribute__ ((__vector_size__ (16), aligned(4), __may_alias__));
    i32x4 x = *(i32x4*)a, cmp, tmp, y0 = y0^y0, y1 = y0, y2 = y0;
    //start vectors off at 0 and add the int to each element for optimization
    //it adds the int to each element, but since we started it at zero,
    //a good compiler (not ICC at -O3) will skip the xor and add and just broadcast/whatever
    y0 += b[0];
    y1 += b[1];
    y2 += b[2];
    cmp =  x == y0;
    tmp =  x == y1; //ppc complains if we don't use temps here
    cmp |= tmp;
    tmp =  x ==y2;
    cmp |= tmp;
    //now hack off then end since we only need 3
    cmp &= (i32x4){0xffffffff,0xffffffff,0xffffffff,0};
    return hastrue128(&cmp);
}

int cmp4(  int* a  ,  int* b ){
typedef __INT32_TYPE__ i32x4 __attribute__ ((__vector_size__ (16), aligned(4), __may_alias__));
    i32x4 x = *(i32x4*)a, cmp, tmp, y0 = y0^y0, y1 = y0, y2 = y0, y3 = y0;
    y0 += b[0];
    y1 += b[1];
    y2 += b[2];
    y3 += b[3];
    cmp =  x == y0;
    tmp =  x == y1; //ppc complains if we don't use temps here
    cmp |= tmp;
    tmp =  x ==y2;
    cmp |= tmp;
    tmp =  x ==y3;
    cmp |= tmp;
    return hastrue128(&cmp);
}

在 arm64 上,它编译为以下无分支代码:

cmp3:
        ldr     q2, [x0]
        adrp    x2, .LC0
        ld1r    {v1.4s}, [x1]
        ldp     w0, w1, [x1, 4]
        dup     v0.4s, w0
        cmeq    v1.4s, v2.4s, v1.4s
        dup     v3.4s, w1
        ldr     q4, [x2, #:lo12:.LC0]
        cmeq    v0.4s, v2.4s, v0.4s
        cmeq    v2.4s, v2.4s, v3.4s
        orr     v0.16b, v1.16b, v0.16b
        orr     v0.16b, v0.16b, v2.16b
        and     v0.16b, v0.16b, v4.16b
        uaddlv h0,v0.16b
        umov    w0, v0.h[0]
        uxth    w0, w0
        ret
cmp4:
        ldr     q2, [x0]
        ldp     w2, w0, [x1, 4]
        dup     v0.4s, w2
        ld1r    {v1.4s}, [x1]
        dup     v3.4s, w0
        ldr     w1, [x1, 12]
        dup     v4.4s, w1
        cmeq    v1.4s, v2.4s, v1.4s
        cmeq    v0.4s, v2.4s, v0.4s
        cmeq    v3.4s, v2.4s, v3.4s
        cmeq    v2.4s, v2.4s, v4.4s
        orr     v0.16b, v1.16b, v0.16b
        orr     v0.16b, v0.16b, v3.16b
        orr     v0.16b, v0.16b, v2.16b
        uaddlv h0,v0.16b
        umov    w0, v0.h[0]
        uxth    w0, w0
        ret

在 ICC x86_64 -march=skylake 上,它会生成以下无分支代码:

cmp3:
        vmovdqu   xmm2, XMMWORD PTR [rdi]                       #27.24
        vpbroadcastd xmm0, DWORD PTR [rsi]                      #34.17
        vpbroadcastd xmm1, DWORD PTR [4+rsi]                    #35.17
        vpcmpeqd  xmm5, xmm2, xmm0                              #34.17
        vpbroadcastd xmm3, DWORD PTR [8+rsi]                    #37.16
        vpcmpeqd  xmm4, xmm2, xmm1                              #35.17
        vpcmpeqd  xmm6, xmm2, xmm3                              #37.16
        vpor      xmm7, xmm4, xmm5                              #36.5
        vpor      xmm8, xmm6, xmm7                              #38.5
        vpand     xmm9, xmm8, XMMWORD PTR __$U0.0.0.2[rip]      #40.5
        vpmovmskb eax, xmm9                                     #11.12
        ret                                                     #41.12
cmp4:
        vmovdqu   xmm3, XMMWORD PTR [rdi]                       #46.24
        vpbroadcastd xmm0, DWORD PTR [rsi]                      #51.17
        vpbroadcastd xmm1, DWORD PTR [4+rsi]                    #52.17
        vpcmpeqd  xmm6, xmm3, xmm0                              #51.17
        vpbroadcastd xmm2, DWORD PTR [8+rsi]                    #54.16
        vpcmpeqd  xmm5, xmm3, xmm1                              #52.17
        vpbroadcastd xmm4, DWORD PTR [12+rsi]                   #56.16
        vpcmpeqd  xmm7, xmm3, xmm2                              #54.16
        vpor      xmm8, xmm5, xmm6                              #53.5
        vpcmpeqd  xmm9, xmm3, xmm4                              #56.16
        vpor      xmm10, xmm7, xmm8                             #55.5
        vpor      xmm11, xmm9, xmm10                            #57.5
        vpmovmskb eax, xmm11                                    #11.12
        ret

它甚至可以在 altivec 的 ppc64 上工作 - 虽然绝对不是最佳的

cmp3:
        lwa 10,4(4)
        lxvd2x 33,0,3
        vspltisw 11,-1
        lwa 9,8(4)
        vspltisw 12,0
        xxpermdi 33,33,33,2
        lwa 8,0(4)
        stw 10,-32(1)
        addi 10,1,-80
        stw 9,-16(1)
        li 9,32
        stw 8,-48(1)
        lvewx 0,10,9
        li 9,48
        xxspltw 32,32,3
        lvewx 13,10,9
        li 9,64
        vcmpequw 0,1,0
        lvewx 10,10,9
        xxsel 32,44,43,32
        xxspltw 42,42,3
        xxspltw 45,45,3
        vcmpequw 13,1,13
        vcmpequw 1,1,10
        xxsel 45,44,43,45
        xxsel 33,44,43,33
        xxlor 32,32,45
        xxlor 32,32,33
        vsldoi 1,12,11,12
        xxland 32,32,33
        vcmpequw. 0,0,12
        mfcr 3,2
        rlwinm 3,3,25,1
        cntlzw 3,3
        srwi 3,3,5
        blr
cmp4:
        lwa 10,8(4)
        lxvd2x 33,0,3
        vspltisw 10,-1
        lwa 9,12(4)
        vspltisw 11,0
        xxpermdi 33,33,33,2
        lwa 7,0(4)
        lwa 8,4(4)
        stw 10,-32(1)
        addi 10,1,-96
        stw 9,-16(1)
        li 9,32
        stw 7,-64(1)
        stw 8,-48(1)
        lvewx 0,10,9
        li 9,48
        xxspltw 32,32,3
        lvewx 13,10,9
        li 9,64
        xxspltw 45,45,3
        vcmpequw 13,1,13
        xxsel 44,43,42,45
        lvewx 13,10,9
        li 9,80
        vcmpequw 0,1,0
        xxspltw 45,45,3
        xxsel 32,43,42,32
        vcmpequw 13,1,13
        xxlor 32,32,44
        xxsel 45,43,42,45
        lvewx 12,10,9
        xxlor 32,32,45
        xxspltw 44,44,3
        vcmpequw 1,1,12
        xxsel 33,43,42,33
        xxlor 32,32,33
        vcmpequw. 0,0,11
        mfcr 3,2
        rlwinm 3,3,25,1
        cntlzw 3,3
        srwi 3,3,5
        blr

从生成的asm可以看出,还有一点改进的空间,但是会在risc-v、mips、ppc等支持向量扩展的架构+编译器组合上编译。

【讨论】:

  • 您可能想使用aligned(4) 作为属性之一,或者添加注释int*a 必须对齐。此外,您不需要 may_alias 从 int 向量加载 int; GNU C 原生向量已经允许这样做。
  • 你能写出cmp4 的方式来处理*b 的负载并以4 种不同的方式洗牌吗?也许通过向量加载,然后通过索引向量而不是直接b[] 来构造 4 个向量。我想如果 AVX 可用,您可能实际上想要 4x vbroadcastss 或 4x vpbroadcastd 而不是 1 load + 4 vpshufd。所以这是一个错过的优化,理论上你不应该需要来帮助 gcc。让 gcc 将 cmp3 编译成一个 pmovmskb / scalar and 而不是向量 AND / shift/shift 可能需要更改源(以更改返回值)。
  • 感谢您的 cmets @PeterCordes 我将添加对齐(一旦我在 gcc 以外的几个编译器中验证语法)IIRC,它在为 AVX2 编译时确实使用了广播操作 - 我在编译时使用了通用 x86_64上面的代码。至少有了这些通用向量扩展,架构之间有一些兼容性,并且在受支持较少的架构上还有未来改进的空间——我仍然无法哄骗 godbolt 在 MIPS 上发出像样的 simd 代码,但是生成的代码(虽然很大)可以工作 并且可能会在以后的编译器中得到改进。
  • 在 clang 上对齐的作品。 godbolt.org/z/5HB7nH。我们得到movdqu 负载,就像我们想要的那样。我很确定它适用于 ICC,但您的原件不适用于 ICC,所以我无法用它进行测试。但我不知道为什么不。它抱怨==|= 的向量没有相同的元素类型。 gcc 也将它编译为 PowerPC 上的 SIMD 指令,但我几乎没有看 asm 来看看它是否合适。
猜你喜欢
  • 2015-04-16
  • 1970-01-01
  • 1970-01-01
  • 2021-08-31
  • 2023-04-02
  • 1970-01-01
  • 2012-07-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多