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

时间:2017-02-02 11:11:56

标签: 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版本的I则需要210 ns。

1 个答案:

答案 0 :(得分:2)

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

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

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

第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调用。这里的教训是始终查看生成的程序集。