使用AVX矢量内在函数手动矢量化的运行速度与Ryzen上添加的4个标量FP的速度大致相同?

所以我决定看看如何通过英特尔® Intrinsics 在 C 中使用 SSE、AVX 等。不是因为有任何实际兴趣将它用于某事,而是出于纯粹的好奇心。试图检查使用 AVX 的代码是否实际上比非 AVX 代码快,结果让我有点惊讶。这是我的 C 代码:

#include <stdio.h>
#include <stdlib.h>

#include <emmintrin.h>
#include <immintrin.h>


/*** Sum up two vectors using AVX ***/
#define __vec_sum_4d_d64(src_vec1, src_vec2, dst_vec) 
  _mm256_store_pd(dst_vec, _mm256_add_pd(_mm256_load_pd(src_vec1), _mm256_load_pd(src_vec2)));

/*** Sum up two vectors without AVX ***/
#define __vec_sum_4d(src_vec1, src_vec2, dst_vec) 
  dst_vec[0] = src_vec1[0] + src_vec2[0];
  dst_vec[1] = src_vec1[1] + src_vec2[1];
  dst_vec[2] = src_vec1[2] + src_vec2[2];
  dst_vec[3] = src_vec1[3] + src_vec2[3];


int main (int argc, char *argv[]) {
  unsigned long i;

  double dvec1[4] = {atof(argv[1]), atof(argv[2]), atof(argv[3]), atof(argv[4])};
  double dvec2[4] = {atof(argv[5]), atof(argv[6]), atof(argv[7]), atof(argv[8])}; 

#if 1
  for (i = 0; i < 3000000000; i++) {
    __vec_sum_4d(dvec1, dvec2, dvec2);
  }
#endif
#if 0
  for (i = 0; i < 3000000000; i++) {
    __vec_sum_4d_d64(dvec1, dvec2, dvec2);
  }
#endif

  printf("%10.10lf %10.10lf %10.10lf %10.10lfn", dvec2[0], dvec2[1], dvec2[2], dvec2[3]);
}

我只是切换#if 1#if 0“模式”(AVX 和非 AVX)之间切换。我的期望是,使用 AVX 的循环至少会比另一个快一些,但事实并非如此。我用gcc version 10.2.0 (GCC)这些编译了代码:-O2 --std=gnu99 -lm -mavx2标志。

#include <stdio.h>
#include <stdlib.h>

#include <emmintrin.h>
#include <immintrin.h>


/*** Sum up two vectors using AVX ***/
#define __vec_sum_4d_d64(src_vec1, src_vec2, dst_vec) 
  _mm256_store_pd(dst_vec, _mm256_add_pd(_mm256_load_pd(src_vec1), _mm256_load_pd(src_vec2)));

/*** Sum up two vectors without AVX ***/
#define __vec_sum_4d(src_vec1, src_vec2, dst_vec) 
  dst_vec[0] = src_vec1[0] + src_vec2[0];
  dst_vec[1] = src_vec1[1] + src_vec2[1];
  dst_vec[2] = src_vec1[2] + src_vec2[2];
  dst_vec[3] = src_vec1[3] + src_vec2[3];


int main (int argc, char *argv[]) {
  unsigned long i;

  double dvec1[4] = {atof(argv[1]), atof(argv[2]), atof(argv[3]), atof(argv[4])};
  double dvec2[4] = {atof(argv[5]), atof(argv[6]), atof(argv[7]), atof(argv[8])}; 

#if 1
  for (i = 0; i < 3000000000; i++) {
    __vec_sum_4d(dvec1, dvec2, dvec2);
  }
#endif
#if 0
  for (i = 0; i < 3000000000; i++) {
    __vec_sum_4d_d64(dvec1, dvec2, dvec2);
  }
#endif

  printf("%10.10lf %10.10lf %10.10lf %10.10lfn", dvec2[0], dvec2[1], dvec2[2], dvec2[3]);
}

如您所见,它们几乎以相同的速度运行。我还尝试将迭代次数增加 10 倍,但结果只会按比例增加。另请注意,两个可执行文件的打印输出值相同,因此我认为最好说两者都执行相同的计算。深入挖掘,我查看了程序集,更加困惑了。以下是两者的重要部分(仅循环):

; With avx
1070:   c5 fd 58 c1             vaddpd %ymm1,%ymm0,%ymm0
1074:   48 83 e8 01             sub    $0x1,%rax
1078:   75 f6                   jne    1070

; Without avx
1080:   c5 fb 58 c4             vaddsd %xmm4,%xmm0,%xmm0
1084:   c5 f3 58 cd             vaddsd %xmm5,%xmm1,%xmm1
1088:   c5 eb 58 d7             vaddsd %xmm7,%xmm2,%xmm2
108c:   c5 e3 58 de             vaddsd %xmm6,%xmm3,%xmm3
1090:   48 83 e8 01             sub    $0x1,%rax
1094:   75 ea                   jne    1080

根据我的理解,第二个应该慢得多,因为除了递减计数器和条件跳转之外,其中的指令数量是其四倍。为什么不慢?是vaddsd指令的速度比只有4次vaddpd

如果这是相关的,我的系统在AMD Ryzen 5 2600X Six-Core Processor支持 AVX 的系统上运行。

回答

使用 AVX

; With avx
1070:   c5 fd 58 c1             vaddpd %ymm1,%ymm0,%ymm0
1074:   48 83 e8 01             sub    $0x1,%rax
1078:   75 f6                   jne    1070

该循环ymm0用作累加器。换句话说,它正在执行ymm0 += ymm1(这是一个向量操作;一次添加 4 个双精度值)。因此它具有循环携带的依赖性ymm0(每个新添加都必须等待前一个添加完成并使用结果开始下一个添加)。vaddpdZen+ 的延迟 = 3,吞吐量 = 1(根据https://www.uops.info/table.html)。循环携带的依赖性使该循环的延迟成为 的瓶颈vaddpd,因此您的循环最多可以获得 3 个循环/迭代。vaddpdCPU 中只有一个附加功能在运行中,这在很大程度上未充分利用其功能。

为了更快地添加更多的累加器(有更多的向量要求和)。它可以(理论上)由于流水线(3 个完整的ymm添加过程)而快 3 倍,只要它不受其他东西的限制。

没有 AVX

; Without avx
1080:   c5 fb 58 c4             vaddsd %xmm4,%xmm0,%xmm0
1084:   c5 f3 58 cd             vaddsd %xmm5,%xmm1,%xmm1
1088:   c5 eb 58 d7             vaddsd %xmm7,%xmm2,%xmm2
108c:   c5 e3 58 de             vaddsd %xmm6,%xmm3,%xmm3
1090:   48 83 e8 01             sub    $0x1,%rax
1094:   75 ea                   jne    1080

该循环将结果累加到 4 个不同的累加器中。基本上它是这样做的:

xmm0 += xmm4
xmm1 += xmm5
xmm2 += xmm7
xmm3 += xmm6

所有这些加法都是相互独立的(并且它们是标量加法,因此每个加法都只对单个 64 位浮点值进行操作)。vaddsd延迟=3,吞吐量=0.5(每条指令周期数)。这意味着它可以在一个周期内开始执行前 2 次加法。然后在下一个循环中,它将开始第二对加法。因此,可以根据吞吐量为该循环实现 2 个周期/迭代。但是延迟,你记得是 3 个周期。所以这个循环在延迟上也有瓶颈。展开一次(使用 4 个额外的累加器;或者通过在将其添加到主累加器之前在彼此之间添加 xmm4-7 来中断循环内的循环携带的 dep.chain)以摆脱瓶颈(它可能会加快约 50%) .

请注意,此(“无 AVX”)反汇编仍在使用 VEX 编码,因此技术上仍需要支持 AVX 的 CPU。

关于基准测试

请注意,您的反汇编没有任何加载或存储,因此这可能代表也可能不代表添加 2 个 4 双向量数组的性能比较。

  • On Zen1, 256-bit math instructions decode to 2 uops (i.e. they split YMM registers into two 128-bit halves). There could maybe be a front-end effect going on there; I'd have expected the YMM version to be at least as fast since it has the same latency but runs half as many uops. IDK, maybe scheduling keeps the halves tied together somewhat so it makes it possible to "lose cycles" on that one critical path? (If that's happening, probably unrolling with even more than 3 accumulators would be good to give scheduling some slack, like in [this Q&A](https://stackoverflow.com/q/45113527/224132)

以上是使用AVX矢量内在函数手动矢量化的运行速度与Ryzen上添加的4个标量FP的速度大致相同?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>