x86 - 为什么我的 AVX2 水平加法功能不比非 SIMD 加法快?

标签 x86 simd intrinsics avx2

我实现了一个内联函数来添加向量的所有元素,但它并不比非 SIMD 加法快。

声明:

#define N 128
#define M N
int __attribute__(( aligned(32)))temp8[8];
__m256i vec;
int __attribute__(( aligned(32))) c_result[N][M];

这是我在向量中添加所有 int 值的两种方法:

首先,非SIMD版本是:

  _mm256_store_si256((__m256i *)&temp8[0] , vec);
  c_result[i][j]= temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7];

二、AVX2版本:

  c_result[i][j] =_mm256_hadd2_epi32(vec);

我以这种方式实现hadd2:

// my horizontal addition of epi32
  inline int _mm256_hadd2_epi32(__m256i a)
  {
    __m256i a_hi;
    a_hi = _mm256_permute2x128_si256(a, a, 1); //maybe 1 should be 4 
    a = _mm256_hadd_epi32(a, a_hi);
    a = _mm256_hadd_epi32(a, a);
    a = _mm256_hadd_epi32(a, a);
    return _mm256_extract_epi32(a,0);
  }

我使用 gccLinux-mintskylake 微架构。

我猜测可能是以下几个原因: Skylake 微架构中有 4 个用于整数的 ALU,与受限向量执行单元相比,它们可以快速添加它们,特别是对于需要至少一个周期来对元素重新排序的排列,然后是一些 hadd说明。问题是,我是否遗漏了某些内容,或者没有必要使用 SIMD 来添加所有元素?

更新:我刚刚将 MUL 程序添加到存储库 here您可以访问矩阵乘法的全部代码。如果我使用非 SIMD 版本,则耗时将为 201 ns,而使用 SIMD 版本,则需要 210 ns。

最佳答案

直觉可能是这一步......

temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7]

是矢量化应该加速的昂贵部分,但这可能是错误的。加法是单个 muop,只要您处理寄存器(而不是内存),您就可以在最新的 x64 机器上每个周期执行 4 个 muop。所以,理论上,你的处理器可以做到这一点......

周期 1。

temp8[0]+temp8[1]
temp8[2]+temp8[3]
temp8[4]+temp8[5]
temp8[6]+temp8[7]

周期2

(temp8[0]+temp8[1])+(temp8[2]+temp8[3]) 
(temp8[4]+temp8[5])+(temp8[6]+temp8[7])

并在第 3 个周期得到答案,并有剩余容量。 (我们的处理器是超标量的,并且有一个无序的管道,所以这会神奇地发生。)

矢量化方法可以快多少?您给了我们答案...

a = _mm256_hadd_epi32(a, a_hi);
a = _mm256_hadd_epi32(a, a);
a = _mm256_hadd_epi32(a, a);

我们可以识别 3 个周期...当然,它看起来可能更便宜...但是 _mm256_hadd_epi32 内在函数下面可能是 PHADD 指令( ~3 muops,每两个周期 1 条指令)。重要的一点是,处理器无法同时执行多个 _mm256_hadd_epi32 内在函数,但它可以同时执行多个标量加法。因此,你看,哪个更快成为一个技术问题。

无论如何,总结一下我的答案......您不应该期望矢量化在这种情况下有所帮助(至少没有多大帮助),因为它不利于廉价指令的超标量执行(添加)。

附录。这段代码

  _mm256_store_si256((__m256i *)&temp8[0] , vec);
  c_result[i][j]= temp8[0]+temp8[1]+temp8[2]+temp8[3]+temp8[4]+temp8[5]+temp8[6]+temp8[7];

可能不会按照您的想法进行编译。让我们把它作为一个函数来冲洗

uint32_t hadd32(__m256i vector) {
  uint32_t buffer[sizeof(__m256i)/sizeof(uint32_t)];
 _mm256_store_si256((__m256i *)buffer , vector);
 uint32_t answer = buffer[0]+buffer[1]+buffer[2]+buffer[3]+buffer[4]+buffer[5]+buffer[6]+buffer[7];
 return answer;
}

几个编译器(clang、GCC 7),将其编译为

    vpextrd edx, xmm0, 1
    vmovd   eax, xmm0
    add     eax, edx
    vpextrd edx, xmm0, 2
    add     eax, edx
    vpextrd edx, xmm0, 3
    vextracti128    xmm0, ymm0, 0x1
    add     eax, edx
    vmovd   edx, xmm0
    add     eax, edx
    vpextrd edx, xmm0, 1
    add     eax, edx
    vpextrd edx, xmm0, 2
    add     eax, edx
    vpextrd edx, xmm0, 3
    add     eax, edx

我们识别出添加内容,但临时缓冲区完全被忽略,以支持 vpextrd 调用。这里的教训是始终查看生成的程序集。

关于x86 - 为什么我的 AVX2 水平加法功能不比非 SIMD 加法快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42000693/

相关文章:

process - 系统如何选择合适的页表?

将代码转换为 Neon 程序集

c - 从 128 位 __m128 Intel Intrinsic 中求和 4 个整数

c - 通过 Visual C++ 编译器识别 ARM 软件中断 (SWI) 函数

c - 从两个 128 位 block 中收集四个 32 位字

assembly - 将 2 个像素点在一起

operating-system - 软件初始化代码位于0xFFFFFFF0H

c - ELF 二进制分析静态与动态。汇编代码如何|指令内存映射变化?

c++ - 测试 AVX 寄存器是否包含一些相等的整数

c++ - 在 sse vector m128 中找到最大 float