AVX中的水平XOR

Ser*_*tch 7 c++ x86 assembly simd avx

有没有办法水平异步AVX寄存器 - 具体来说,对256位寄存器的四个64位组件进行异或?

目标是获得AVX寄存器的所有4个64位组件的XOR.它基本上与水平add(_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];
}
Run Code Online (Sandbox Code Playgroud)

Cod*_*ray 12

正如评论中所述,最快的代码很可能使用标量运算,在整数寄存器中执行所有操作.你需要做的就是提取四个打包的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];
}
Run Code Online (Sandbox Code Playgroud)

假设tymm1,所产生的拆卸将是这样的:

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
Run Code Online (Sandbox Code Playgroud)

...结果留在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));
}
Run Code Online (Sandbox Code Playgroud)

然后你将得到以下反汇编(再次,假设它xymm1):

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
Run Code Online (Sandbox Code Playgroud)

请注意,我们能够忽略一条提取指令,并且我们已经确保VEXTRACTI128使用而不是VEXTRACTF128(尽管这种选择可能并不重要).

您将在其他编译器上看到类似的输出.例如,这里是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
Run Code Online (Sandbox Code Playgroud)

有相同的说明,但它们已经略有重新排序.内在函数允许编译器的调度程序按其认为最佳的顺序进行排序.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
Run Code Online (Sandbox Code Playgroud)

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


另一方面,如果您希望结果在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)));
}
Run Code Online (Sandbox Code Playgroud)

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

说到这一点,我不确定你是如何让自己陷入这样一种情况,你需要首先进行这样的横向操作.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)));
}
Run Code Online (Sandbox Code Playgroud)

这仍然通过一次执行两次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
Run Code Online (Sandbox Code Playgroud)

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

vperm2f128  ymm0, ymm1, ymm1, 1    ; input is in YMM1
vpxor       ymm2, ymm0, ymm1
vpshufd     ymm1, ymm2, 78
vpxor       ymm0, ymm1, ymm2
Run Code Online (Sandbox Code Playgroud)

(在我第一次发布这个答案后,我在上床睡觉时想出了这个,并计划回来更新答案,但是我发现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
Run Code Online (Sandbox Code Playgroud)

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

  • 为了交换ymm寄存器的两个通道,`vpermq`应优先于`vperm2i128`.它只有一个输入,这使得它在Ryzen和KNL上更快.它们与Intel Haswell/Skylake的性能相同. (2认同)
  • 当然,在Ryzen上,`vextracti128`甚至更好,128b操作只是一个单一的uop.如果您不需要将结果广播到所有元素,那么尽早缩小到128b对于水平操作通常是一个很好的策略,包括这个.但是`vpextrq`在uop计数方面相对较贵,所以将xfl寄存器底部的标量随机/ xor向下移动到有效位置,然后使用一个`vmovq`(到整数寄存器或内存).这同样适用于其他水平操作,[包括整数和](/sf/answers/2468901851/). (2认同)