AVX中的水平XOR

时间:2017-07-05 21:00:21

标签: c++ assembly x86 simd avx

有没有办法水平对AVX寄存器进行异或,特别是对256位寄存器的4个64位组件进行异或?

目标是获得AVX寄存器的所有4个64位组件的XOR。它基本上与水平加法(_mm256_hadd_epi32())做同样的事情,除了我想要XOR而不是ADD。

标量代码是:

inline uint64_t HorizontalXor(__m256i t) {
  return t.m256i_u64[0] ^ t.m256i_u64[1] ^ t.m256i_u64[2] ^ t.m256i_u64[3];
}

3 个答案:

答案 0 :(得分:10)

如评论中所述,最快的代码很可能使用标量运算,在整数寄存器中执行所有操作。你需要做的就是提取四个打包的64位整数,然后你有三个XOR指令,你就完成了。这可以非常有效地完成,并将结果留在整数寄存器中,这是您的示例代码建议您想要的。

MSVC已经为您在问题中显示的标量函数生成了相当不错的代码:

inline uint64_t HorizontalXor(__m256i t) {
  return t.m256i_u64[0] ^ t.m256i_u64[1] ^ t.m256i_u64[2] ^ t.m256i_u64[3];
}

假设t位于ymm1,生成的反汇编将是这样的:

vextractf128 xmm0, ymm1, 1
vpextrq      rax,  xmm0, 1
vmovq        rcx,  xmm1
xor          rax,  rcx
vpextrq      rcx,  xmm1, 1
vextractf128 xmm0, ymm1, 1
xor          rax,  rcx
vmovq        rcx,  xmm0
xor          rax,  rcx

...结果保留在RAX中。如果这准确反映了您的需求(标量uint64_t结果),则此代码就足够了。

您可以使用内在函数稍微改进它:

inline uint64_t _mm256_hxor_epu64(__m256i x)
{
   const __m128i temp = _mm256_extracti128_si256(x, 1);
   return (uint64_t&)x
          ^ (uint64_t)(_mm_extract_epi64(_mm256_castsi256_si128(x), 1))
          ^ (uint64_t&)(temp)
          ^ (uint64_t)(_mm_extract_epi64(temp, 1));
}

然后您将获得以下反汇编(同样,假设x位于ymm1):

vextracti128 xmm2, ymm1, 1
vpextrq      rcx,  xmm2, 1
vpextrq      rax,  xmm1, 1
xor          rax,  rcx
vmovq        rcx,  xmm1
xor          rax,  rcx
vmovq        rcx,  xmm2
xor          rax,  rcx

请注意,我们能够忽略一条提取指令,并确保使用VEXTRACTI128代替VEXTRACTF128(尽管this choice probably does not matter)。

您将在其他编译器上看到类似的输出。例如,这里是GCC 7.1(x假设为ymm0):

vextracti128 xmm2, ymm0, 0x1
vpextrq      rax,  xmm0, 1
vmovq        rdx,  xmm2
vpextrq      rcx,  xmm2, 1
xor          rax,  rdx
vmovq        rdx,  xmm0
xor          rax,  rdx
xor          rax,  rcx

有相同的说明,但它们已经稍微重新排序了。内在函数允许编译器的调度程序按其认为最佳的顺序进行排序。 Clang 4.0以不同的方式安排它们:

vmovq        rax,  xmm0
vpextrq      rcx,  xmm0, 1
xor          rcx,  rax
vextracti128 xmm0, ymm0, 1
vmovq        rdx,  xmm0
xor          rdx,  rcx
vpextrq      rax,  xmm0, 1
xor          rax,  rdx

当然,当代码内联时,这种排序总是会发生变化。

另一方面,如果您希望结果在AVX寄存器中,那么您首先需要决定如何存储它。我想你只会将单个64位结果存储为标量,如:

inline __m256i _mm256_hxor(__m256i x)
{
   const __m128i temp = _mm256_extracti128_si256(x, 1);
   return _mm256_set1_epi64x((uint64_t&)x
                             ^ (uint64_t)(_mm_extract_epi64(_mm256_castsi256_si128(x), 1))
                             ^ (uint64_t&)(temp)
                             ^ (uint64_t)(_mm_extract_epi64(temp, 1)));
}

但是现在你正在进行大量的数据改组,否定了从矢量化代码中可能看到的任何性能提升。

说到这一点,我不确定你是如何让自己陷入这样一种情况,你需要首先进行这样的横向操作。 SIMD操作旨在垂直缩放 ,而不是水平缩放。如果您仍处于实施阶段,则可能需要重新考虑设计。特别是,您应该在4个不同的 AVX寄存器中生成4个整数值,而不是将它们全部打包成一个。

如果您确实希望将 4份的结果打包到AVX寄存器中,那么您可以这样做:

inline __m256i _mm256_hxor(__m256i x)
{
   const __m256i temp = _mm256_xor_si256(x,
                                         _mm256_permute2f128_si256(x, x, 1));    
   return _mm256_xor_si256(temp,
                           _mm256_shuffle_epi32(temp, _MM_SHUFFLE(1, 0, 3, 2)));
}

这仍然通过一次执行两次XOR来利用一点并行性,这意味着只需要两次XOR操作,而不是三次。

如果它有助于可视化,这基本上可以:

   A         B         C         D           ⟵ input
  XOR       XOR       XOR       XOR
   C         D         A         B           ⟵ permuted input
=====================================
  A^C       B^D       A^C        B^D         ⟵ intermediate result
  XOR       XOR       XOR        XOR
  B^D       A^C       B^D        A^C         ⟵ shuffled intermediate result
======================================
A^C^B^D   A^C^B^D   A^C^B^D    A^C^B^D      ⟵ final result

在几乎所有编译器上,这些内在函数将生成以下汇编代码:

vperm2f128  ymm0, ymm1, ymm1, 1    ; input is in YMM1
vpxor       ymm2, ymm0, ymm1
vpshufd     ymm1, ymm2, 78
vpxor       ymm0, ymm1, ymm2

(我在第一次发布这个答案后的路上想出了这个,并计划回来更新答案,但我看到wim在发布它时打败了我。好吧,它仍然比我最初的方法更好,所以它仍然值得包含在这里。)

当然,如果你想在整数寄存器中使用它,你只需要一个简单的VMOVQ

vperm2f128  ymm0, ymm1, ymm1, 1    ; input is in YMM1
vpxor       ymm2, ymm0, ymm1
vpshufd     ymm1, ymm2, 78
vpxor       ymm0, ymm1, ymm2
vmovq       rax,  xmm0

问题是,这会比上面的标量代码更快。答案是,是的,可能。虽然您使用AVX执行单元进行XOR,而不是完全独立的整数执行单元,但需要完成的AVX shuffles / permutes / extract更少,这意味着开销更少。因此,我可能还需要在标量代码上吃掉我的话,这是最快的实现。但这实际上取决于你正在做什么以及如何安排/交错指令。

答案 1 :(得分:4)

如果水平xor - 函数的输入已经存在,则矢量化可能很有用 一个AVX寄存器,即你的t是一些SIMD计算的结果。 否则,标量代码可能会更快,正如@Cody Gray已经提到的那样。 通常,您可以在log_2(SIMD_width)'步骤'中进行水平SIMD操作。 在这种情况下,一步是“洗牌/置换”。和' xor'。这比@Cody Gray的_mm256_hxor函数稍微有效:

__m256i _mm256_hxor_v2(__m256i x)
{
    __m256i x0 = _mm256_permute2x128_si256(x,x,1);       // swap the 128 bit high and low lane 
    __m256i x1 = _mm256_xor_si256(x,x0);
    __m256i x2 = _mm256_shuffle_epi32(x1,0b01001110);    // swap 64 bit lanes                         
    __m256i x3 = _mm256_xor_si256(x1,x2);
    return x3;
}

这编译为:

vperm2i128  $1, %ymm0, %ymm0, %ymm1
vpxor   %ymm1, %ymm0, %ymm0
vpshufd $78, %ymm0, %ymm1
vpxor   %ymm1, %ymm0, %ymm0


如果您希望将结果存入标量寄存器:

uint64_t _mm256_hxor_v2_uint64(__m256i x)
{
    __m256i x0 = _mm256_permute2x128_si256(x,x,1);
    __m256i x1 = _mm256_xor_si256(x,x0);
    __m256i x2 = _mm256_shuffle_epi32(x1,0b01001110);
    __m256i x3 = _mm256_xor_si256(x1,x2);
    return _mm_cvtsi128_si64x(_mm256_castsi256_si128(x3)) ;
}

编译为:

vperm2i128  $1, %ymm0, %ymm0, %ymm1
vpxor   %ymm1, %ymm0, %ymm0
vpshufd $78, %ymm0, %ymm1
vpxor   %ymm1, %ymm0, %ymm0
vmovq   %xmm0, %rax


完整的测试代码:

#include <stdio.h>
#include <x86intrin.h>
#include <stdint.h>
/*  gcc -O3 -Wall -m64 -march=broadwell hor_xor.c   */
int print_vec_uint64(__m256i v);

__m256i _mm256_hxor_v2(__m256i x)
{
    __m256i x0 = _mm256_permute2x128_si256(x,x,1);
    __m256i x1 = _mm256_xor_si256(x,x0);
    __m256i x2 = _mm256_shuffle_epi32(x1,0b01001110);
    __m256i x3 = _mm256_xor_si256(x1,x2);
/* Uncomment the next few lines to print the values of the intermediate variables */ 
/*
    printf("3...0        =          3          2          1          0\n");
    printf("x            = ");print_vec_uint64(x        );
    printf("x0           = ");print_vec_uint64(x0        );
    printf("x1           = ");print_vec_uint64(x1        );
    printf("x2           = ");print_vec_uint64(x2        );
    printf("x3           = ");print_vec_uint64(x3        );
*/
    return x3;
}

uint64_t _mm256_hxor_v2_uint64(__m256i x)
{
    __m256i x0 = _mm256_permute2x128_si256(x,x,1);
    __m256i x1 = _mm256_xor_si256(x,x0);
    __m256i x2 = _mm256_shuffle_epi32(x1,0b01001110);
    __m256i x3 = _mm256_xor_si256(x1,x2);
    return _mm_cvtsi128_si64x(_mm256_castsi256_si128(x3)) ;
}


int main() {
    __m256i x = _mm256_set_epi64x(0x7, 0x5, 0x2, 0xB);
//    __m256i x = _mm256_set_epi64x(4235566778345231, 1123312566778345423, 72345566778345673, 967856775433457);

    printf("x            = ");print_vec_uint64(x);

    __m256i y = _mm256_hxor_v2(x);

    printf("y            = ");print_vec_uint64(y);

    uint64_t z = _mm256_hxor_v2_uint64(x);

    printf("z =  %10lX  \n",z);

    return 0;
}


int print_vec_uint64(__m256i v){
    uint64_t t[4];
    _mm256_storeu_si256((__m256i *)t,v);
    printf("%10lX %10lX %10lX %10lX \n",t[3],t[2],t[1],t[0]);
    return 0;
}

答案 2 :(得分:2)

对于XOR,_mm256_hadd_epi32()的直接模拟的实现将如下所示:

#include <immintrin.h>

template<int imm> inline __m256i _mm256_shuffle_epi32(__m256i a, __m256i b)
{
    return _mm256_castps_si256(_mm256_shuffle_ps(_mm256_castsi256_ps(a), _mm256_castsi256_ps(b), imm));
}

inline __m256i _mm256_hxor_epi32(__m256i a, __m256i b)
{
    return _mm256_xor_si256(_mm256_shuffle_epi32<0x88>(a, b), _mm256_shuffle_epi32<0xDD>(a, b));
}

int main()
{
    __m256i a = _mm256_setr_epi32(0, 1, 2, 3, 4, 5, 6, 7);
    __m256i b = _mm256_setr_epi32(1, 2, 3, 4, 5, 6, 7, 8);
    __m256i c = _mm256_hxor_epi32(a, b);
    return 0;
}