【问题标题】:Horizontal XOR in AVXAVX 中的水平异或
【发布时间】:2017-12-09 16:38:28
【问题描述】:

有没有办法对一个 AVX 寄存器进行水平异或,特别是对一个 256 位寄存器的四个 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];
}

【问题讨论】:

  • 没有内置,很容易手动实现。
  • 使用非 SIMD 指令执行此操作可能会更快。你需要三个XORs,你就完成了。 (特别是如果您无论如何都希望将结果保存在整数寄存器中,这就是代码示例所暗示的。)
  • @CodyGray,的确,这是个好主意,谢谢!我认为,这个问题的答案仍然可能对某人有用。
  • @IwillnotexistIdonotexist ,谢谢,我已经将我正在做的事情推到 github.com/srogatch/ProbQA 。它的核心是大立方体:nAnswers * nQuestions * nTargets 和一些包含聚合的维数较小的数组。我目前正在为它实现 CPU 引擎(嗯,它只是 x86_64 引擎,但我还没有为例如 ARM 计划它,超级计算机引擎会有自己的名字),但 CUDA 和网络网格引擎也在计划之中。在数学上它基于贝叶斯公式和朴素贝叶斯假设。

标签: c++ assembly x86 simd avx


【解决方案1】:

如 cmets 中所述,最快的代码很可能使用标量运算,在整数寄存器中执行所有操作。你需要做的就是提取四个压缩的 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];
}

假设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

...结果留在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));
}

然后你会得到以下反汇编(再次假设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

请注意,我们能够省略一条提取指令,并且我们已确保使用 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 洗牌/置换/提取更少,这意味着更少的开销。所以我可能还不得不承认标量代码是最快的实现。但这实际上取决于您在做什么以及如何安排/交错指令。

【讨论】:

  • 关于 XOR 的良好视觉解释!
  • 对于交换 ymm 寄存器的两个通道,vpermq 应该优先于 vperm2i128。它只有一个输入,这使得它在 Ryzen 和 KNL 上更快。它们在 Intel Haswell/Skylake 上的性能相同。
  • 当然,vextracti128 在 Ryzen 上更胜一筹,128b 操作也只是一个 uop。如果您不需要将结果广播到所有元素,那么尽早缩小到 128b 是一般水平操作的好策略,包括这个。但是vpextrq 在 uop 计数方面相对昂贵,因此将 xmm 寄存器底部的一个标量随机/异或向下移动是有意义的,然后使用一个 vmovq(到整数寄存器或内存)。这同样适用于其他水平操作,including integer sums
  • 完全独立的整数执行单元:它们与 Intel CPU 中的向量执行单元位于相同的端口上,但 Haswell 和更高版本的端口 6 具有整数 ALU(以及分支单元),但没有向量执行单元。因此,通过混合标量指令只能获得少量额外的 ALU 吞吐量,但它需要大量的前端吞吐量和 p0 / p5 微指令来获取数据。 (在 AMD 上,这个想法的前端吞吐量也是一个问题,即使整数和向量微指令使用不同的管道)。
  • 值得考虑:内通道vpshufdvpxor ymmvextracti128、2x vmovq、2x 标量xor。 Intel 上总共有 7 个 uops,第一个 vmovq 可以执行,而第二个正在等待 vextracti128 结果。在 Intel 上,延迟并不比您的最终序列好,但它会花费更多的总 uops(需要并行运行以使延迟不会更糟)。所以它不能与周围的代码重叠。
【解决方案2】:

如果水平xor-函数的输入已经在 一个 AVX 寄存器,即您的 t 是一些 SIMD 计算的结果。 否则,正如@Cody Gray 已经提到的那样,标量代码可能会更快。 通常您可以在大约 log_2(SIMD_width) 'steps' 中执行水平 SIMD 操作。 在这种情况下,一个步骤是“洗牌/置换”和“异或”。这比@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;
}

【讨论】:

  • 确实,我原来的解决方案是次优的。我在晚上睡觉前发布了答案,然后在我睡觉的路上意识到了一个更好的解决方案。回来更新,我看到你已经发布了。为了完整起见,我继续更新了我的答案,但请投赞成票!
  • @CodyGray 简单地说,“简单”水平运算的 SIMD 复杂度,例如水平求和、乘积、最小值、最大值、逻辑与等,通常是 O(log(n)) 而不是O(n),其中 n 是 SIMD 寄存器中的元素数。有时这很明显,例如horizontal minimum。有时它不太明显,such as this one
  • my comments on Cody's update 的大部分内容也适用于此处:第一步降低到 128b(在 Ryzen 和 Excavator 上更快),并在不需要时避免使用 vperm2i128vextracti128 在 Ryzen 上表现出色,vpermq 在交换上/下通道方面优于 vperm2?128
  • 当您确实想要将结果广播到每个元素而不是减少到 128 然后是标量时,首先进行通道内随机播放可能稍微 更好,因为较低的延迟意味着更多的微指令/指令可以更快地执行(和退出),从而释放保留站和 ROB 中的空间。甚至构建一个可以测量差异的人工测试可能并非易事,但我认为它不会受到伤害。这在减少到标量时也适用,但在这种情况下,保持 256b 的时间更长意味着 AMD CPU 上的微操作数增加,所以我建议先减少到 128b。
  • :) 即使没有锐龙,据推测尽快缩小到 128b 具有能源/功率优势。可能与 FP add 比 XOR 更相关,但仍然很小。此外,速度优势例如如果 CPU 仍处于 AVX“热身”模式,上层通道尚未激活,则 Skylake。
【解决方案3】:

为 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;
}

【讨论】:

  • 我已编辑问题以阐明目标。敬请期待。另外,我担心上面的代码比 __m256i 寄存器的 XOR 64 位组件要慢:4 个组件需要 3 个标量 XOR 操作。
  • @SergeRogatch 你能写出你想用 AVX 优化的标量代码吗?
猜你喜欢
  • 1970-01-01
  • 2014-06-05
  • 2018-12-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-29
相关资源
最近更新 更多