如何使用SIMD加速XOR两块内存?

foo*_*ool 6 c simd xor

我想尽可能快地对两块内存进行异或,我如何使用SIMD来加速它?

我的原始代码如下:

void region_xor_w64(   unsigned char *r1,         /* Region 1 */
                       unsigned char *r2,         /* Region 2 */
                       int nbytes)       /* Number of bytes in region */
{
    uint64_t *l1;
    uint64_t *l2;
    uint64_t *ltop;
    unsigned char *ctop;

    ctop = r1 + nbytes;
    ltop = (uint64_t *) ctop;
    l1 = (uint64_t *) r1;
    l2 = (uint64_t *) r2;

    while (l1 < ltop) {
        *l2 = ((*l1)  ^ (*l2));
        l1++;
        l2++;
    }
}
Run Code Online (Sandbox Code Playgroud)

我自己写了一个,但速度很快.

void region_xor_sse(   unsigned char* dst,
                       unsigned char* src,
                       int block_size){
  const __m128i* wrd_ptr = (__m128i*)src;
  const __m128i* wrd_end = (__m128i*)(src+block_size);
  __m128i* dst_ptr = (__m128i*)dst;

  do{
    __m128i xmm1 = _mm_load_si128(wrd_ptr);
    __m128i xmm2 = _mm_load_si128(dst_ptr);

    xmm2 = _mm_xor_si128(xmm1, xmm2);
    _mm_store_si128(dst_ptr, xmm2);
    ++dst_ptr;
    ++wrd_ptr;
  }while(wrd_ptr < wrd_end);
}
Run Code Online (Sandbox Code Playgroud)

小智 8

更重要的问题是为什么要手动完成.你有一个古老的编译器,你认为你可以智取吗?那些必须手动编写SIMD指令的好时光已经结束.今天,在99%的情况下,编译器将为您完成工作,并且可能会做得更好.另外,不要忘记每隔一段时间就有越来越多的扩展指令集出现新的架构.所以问问自己一个问题 - 您是否希望为每个平台维护N个实施副本?您是否希望不断测试您的实施以确保它值得维护?最有可能的答案是否定的.

您唯一需要做的就是编写最简单的代码.编译器将完成剩下的工作.例如,以下是我编写函数的方法:

void region_xor_w64(unsigned char *r1, unsigned char *r2, unsigned int len)
{
    unsigned int i;
    for (i = 0; i < len; ++i)
        r2[i] = r1[i] ^ r2[i];
}
Run Code Online (Sandbox Code Playgroud)

有点简单,不是吗?猜猜看,编译器正在生成使用MOVDQU和执行128位XOR的代码,PXOR关键路径如下所示:

4008a0:       f3 0f 6f 04 06          movdqu xmm0,XMMWORD PTR [rsi+rax*1]
4008a5:       41 83 c0 01             add    r8d,0x1
4008a9:       f3 0f 6f 0c 07          movdqu xmm1,XMMWORD PTR [rdi+rax*1]
4008ae:       66 0f ef c1             pxor   xmm0,xmm1
4008b2:       f3 0f 7f 04 06          movdqu XMMWORD PTR [rsi+rax*1],xmm0
4008b7:       48 83 c0 10             add    rax,0x10
4008bb:       45 39 c1                cmp    r9d,r8d
4008be:       77 e0                   ja     4008a0 <region_xor_w64+0x40>
Run Code Online (Sandbox Code Playgroud)

正如@Mysticial指出的那样,上面的代码使用的是支持未对齐访问的指令.那些比较慢.但是,如果程序员可以正确地采用对齐访问,则可以让编译器知道它.例如:

void region_xor_w64(unsigned char * restrict r1,
                    unsigned char * restrict r2,
                    unsigned int len)
{
    unsigned char * restrict p1 = __builtin_assume_aligned(r1, 16);
    unsigned char * restrict p2 = __builtin_assume_aligned(r2, 16);

    unsigned int i;
    for (i = 0; i < len; ++i)
        p2[i] = p1[i] ^ p2[i];
}
Run Code Online (Sandbox Code Playgroud)

编译器为上面的C代码生成以下内容(注意movdqa):

400880:       66 0f 6f 04 06          movdqa xmm0,XMMWORD PTR [rsi+rax*1]
400885:       41 83 c0 01             add    r8d,0x1
400889:       66 0f ef 04 07          pxor   xmm0,XMMWORD PTR [rdi+rax*1]
40088e:       66 0f 7f 04 06          movdqa XMMWORD PTR [rsi+rax*1],xmm0
400893:       48 83 c0 10             add    rax,0x10
400897:       45 39 c1                cmp    r9d,r8d
40089a:       77 e4                   ja     400880 <region_xor_w64+0x20>
Run Code Online (Sandbox Code Playgroud)

明天,当我给自己买一台带有Haswell CPU的笔记本电脑时,编译器会生成一个代码,使用256位指令而不是相同代码的128位,这样我的矢量性能提高了两倍.即使我不知道Haswell能够做到这一点,它也会这样做.您不仅要了解该功能,还要编写代码的另一个版本并花一些时间对其进行测试.

顺便说一下,您的实现中似乎也有一个错误,其中代码可以跳过数据向量中最多3个剩余字节.

无论如何,我建议您信任您的编译器并学习如何验证生成的内容(即熟悉objdump).下一个选择是更改编译器.然后才开始考虑手动编写矢量处理指令.或者你会度过一段美好的时光!

希望能帮助到你.祝好运!

  • 同时,我会指出,除非您一直使用`__m128i`,否则增加数据类型的大小实际上并不会有所帮助。因为即使64位整数对齐也不足以消除对`movdqu`的需要。 (2认同)
  • 99%的时间都是夸大其词。关于矢量化 gcc 根本不自动矢量化的东西有很多问题。有时 clang 或 ICC 会。或者有时 gcc 会,但 clang 不会。您可以使用 SSE4 / AVX2 做很多事情,而不仅仅是像这样的琐碎的纯垂直的东西。我的意思是,在适当的情况下使用“restrict”来启用自动矢量化是好的,编译器会在这种情况下做得很好。在涉及扩大或缩小的更复杂的情况下,它们有时会做“可怕的”自动矢量化工作,您可以将它们击败 2 倍或更多。 (2认同)