【问题标题】:Manual vectorization using AVX vector intrinsics only runs about the same speed as 4 scalar FP adds on Ryzen?使用 AVX 矢量内在函数的手动矢量化仅运行与 Ryzen 上添加的 4 标量 FP 相同的速度?
【发布时间】:2021-06-10 14:21:13
【问题描述】:

所以我决定看看如何通过英特尔® 内在函数在 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.10lf\n", 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 标志编译了代码。

> time ./noavx.x86_64 1 2 3 4 5 6 7 8
3000000005.0000000000 6000000006.0000000000 9000000007.0000000000 12000000008.0000000000

real    0m2.150s
user    0m2.147s
sys 0m0.000s

> time ./withavx.x86_64 1 2 3 4 5 6 7 8
3000000005.0000000000 6000000006.0000000000 9000000007.0000000000 12000000008.0000000000

real    0m2.168s
user    0m2.165s
sys 0m0.000s

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

; 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 指令是否仅比 vaddpd 快四倍?

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

【问题讨论】:

  • 这似乎是编译器可以在编译时预先计算的东西。关于 SO 的所有基准测试问题中约有 90% 是由错误的基准测试方法引起的。考虑将这两个双精度数组作为函数的参数,然后反汇编该函数。
  • 除了其他cmets,注意可能会受到内存访问速度的限制。
  • ymm 寄存器比 xmm 寄存器宽。但是第二个循环使用更多的寄存器做更多的操作。我相信你的问题的答案是流水线。 CPU 很可能能够使用计算硬件在与一次 ymm 操作相同的时间内运行两次 xmm 操作。
  • @ZanLynx:不完全是:两者都受到 FP add 的 3 周期延迟作为循环携带依赖项的瓶颈,而不是吞吐量限制。一个vaddpd ymm 总体上比4x vaddpd/sd xmm 便宜(2 微指令)(相同的后端端口为4 微指令,前端成本更高)。基本上,每个 YMM 操作与两个 XMM 操作消耗相同的吞吐量资源(以一些可能的前端差异为模),但前端和后端吞吐量都不是瓶颈。
  • @GimbaAghDurba:请注意,gcc -O3 启用自动矢量化,并希望为两个版本制作相同的 asm。或者可能是 2x vaddpd xmm,具体取决于其调整选项。

标签: c assembly x86 cpu-architecture avx


【解决方案1】:

使用 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 具有循环依赖(每个新添加都必须等待前一个添加完成并使用结果开始下一个添加)。对于 Zen+,vaddpd 的延迟=3,吞吐量=1(根据https://www.uops.info/table.html)。循环携带的依赖使得这个循环在vaddpd延迟上成为瓶颈,所以你的循环最多可以得到 3 个循环/迭代。 CPU 中只有一个 vaddpd 添加正在进行中,这大大未充分利用其功能。

为了更快地添加更多的累加器(有更多的向量来求和)。只要不受其他限制,它可以(理论上)由于流水线(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-double 向量数组的性能比较。

【讨论】:

  • 在 Zen1 上,256 位数学指令解码为 2 个微指令(即它们将 YMM 寄存器分成两个 128 位的一半)。那里可能会产生前端效应;我本来希望 YMM 版本至少一样快,因为它具有相同的延迟,但运行的微指令数量只有一半。 IDK,也许调度会在某种程度上将两半联系在一起,这样就可以在那条关键路径上“丢失周期”? (如果发生这种情况,可能展开超过 3 个累加器会很好地给调度一些松弛,例如 this Q&A
  • 哦,它们的速度基本相同,因此两者可能都可靠地达到了延迟瓶颈。也许只是 CPU 频率或其他预热问题,或者某种代码对齐问题?虽然这两个测试不在同一个过程中,所以它不像一个可以为另一个预热 CPU。但是,time 仍然没有那么精确。
  • @PeterCordes 是的,时间看起来很相似。你想弄清楚什么? “可能只是 CPU 频率或其他预热问题,或者某种代码对齐问题”我没有关注。
  • OP 的测量值存在一些微小的时间差异,这可能只是噪音,也可能是由于一些较小的影响。这就是我的猜测。第二个想法是代码对齐不太可能,因为瓶颈不是前端。
  • 当我写我的第一条评论时,我只阅读了问题的第一部分(它说标量更快)并浏览了代码,然后向下滚动以查看是否已经有答案总结发生了什么。有,所以我正在查看这个答案,以解释为什么 AVX 在我写评论时较慢,而不是为什么它们的速度相同。所以这就是为什么我首先试图考虑复杂的解释:P
【解决方案2】:

您正在处理延迟问题。根据 CPU 的不同,您必须等待 3 或 4 个周期才能使用 vaddpdvaddsd 指令的结果。但在 1 个周期内最多可以执行 2 条 vaddpdvaddsd 指令(如果 CPU 不必等待源寄存器)。

因为在你的循环中

; 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 取决于上一次迭代的结果,它必须等待 3 或 4 个周期才能执行。但是所有vaddsdsubjne 的执行都可能在这段时间内发生。因此,对于这个简单的循环,执行一个vaddpd 或四个vaddsd 并没有什么区别。

要完全耗尽vaddpd指令,你需要执行其中不依赖于彼此结果的6或8个(或有其他指令做一些独立的工作)。

【讨论】:

    猜你喜欢
    • 2017-08-31
    • 2010-09-29
    • 1970-01-01
    • 2021-03-07
    • 2014-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-10-11
    相关资源
    最近更新 更多