使用 AVX 矢量内在函数的手动矢量化仅运行与在 Ryzen 上添加 4 个标量 FP 大致相同的速度?
Manual vectorization using AVX vector intrinsics only runs about the same speed as 4 scalar FP adds on Ryzen?
所以我决定看看如何通过英特尔® 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.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 [=12=]x1,%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 [=12=]x1,%rax
1094: 75 ea jne 1080
根据我的理解,第二个应该慢得多,因为除了递减计数器和条件跳转之外,其中还有四倍多的指令。为什么它不慢? vaddsd
指令只比 vaddpd
快四倍吗?
如果这是相关的,我的系统 运行 在支持 AVX 的 AMD Ryzen 5 2600X Six-Core Processor
上。
您正在处理延迟问题。根据 CPU,您必须等待 3 或 4 个周期,直到您可以使用 vaddpd
或 vaddsd
指令的结果。但是在1个周期内最多可以执行2条vaddpd
或vaddsd
指令(如果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 [=10=]x1,%rax
1094: 75 ea jne 1080
每个 vaddsd
取决于上一次迭代的结果,它必须等待 3 或 4 个循环才能执行。但是所有 vaddsd
和 sub
和 jne
的执行都可能在那段时间发生。因此,对于这个简单的循环,执行一个 vaddpd
或四个 vaddsd
.
并没有什么区别
要完全耗尽 vaddpd
指令,您需要执行其中的 6 或 8 个不依赖于彼此结果的指令(或者有其他指令做一些独立的工作)。
使用 AVX
; With avx
1070: c5 fd 58 c1 vaddpd %ymm1,%ymm0,%ymm0
1074: 48 83 e8 01 sub [=10=]x1,%rax
1078: 75 f6 jne 1070
此循环使用 ymm0
作为累加器。换句话说,它正在做 ymm0 += ymm1
(这是一个向量运算;一次添加 4 个双精度值)。因此它对 ymm0
具有循环携带依赖性(每个新添加都必须等待前一个添加完成并使用结果开始下一个添加)。 vaddpd
Zen+ 的延迟=3,吞吐量=1(根据https://www.uops.info/table.html)。循环携带的依赖性使得这个循环瓶颈在 vaddpd
的 latency 上,所以你的循环最多可以得到 3 cycles/iteration。在 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 [=11=]x1,%rax
1094: 75 ea jne 1080
这个循环将结果累加到 4 个不同的累加器中。基本上它在做:
xmm0 += xmm4
xmm1 += xmm5
xmm2 += xmm7
xmm3 += xmm6
所有这些加法彼此独立(并且它们是标量加法,因此每个加法仅对单个 64 位浮点值进行运算)。 vaddsd
延迟=3,吞吐量=0.5(每条指令的周期数)。这意味着它可以在一个周期内开始执行前 2 个加法。然后在下一个循环中它将开始第二对添加。因此,有可能根据吞吐量为该循环实现 2 cycles/iteration。但是延迟,正如你记得的那样是 3 个周期。所以这个循环在延迟上也有瓶颈。展开一次(使用 4 个额外的累加器;或者通过在将 xmm4-7 添加到主累加器之前在彼此之间添加 xmm4-7 来打破循环中的循环携带 dep.chain)以摆脱瓶颈(它可能会达到 ~50%更快)。
请注意,此(“无 AVX”)反汇编仍在使用 VEX 编码,因此技术上仍需要支持 AVX CPU。
关于基准测试
请注意,您的反汇编没有任何加载或存储,因此这可能代表也可能不代表添加 2 个 4 双精度向量数组的性能比较。
所以我决定看看如何通过英特尔® 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.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 [=12=]x1,%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 [=12=]x1,%rax
1094: 75 ea jne 1080
根据我的理解,第二个应该慢得多,因为除了递减计数器和条件跳转之外,其中还有四倍多的指令。为什么它不慢? vaddsd
指令只比 vaddpd
快四倍吗?
如果这是相关的,我的系统 运行 在支持 AVX 的 AMD Ryzen 5 2600X Six-Core Processor
上。
您正在处理延迟问题。根据 CPU,您必须等待 3 或 4 个周期,直到您可以使用 vaddpd
或 vaddsd
指令的结果。但是在1个周期内最多可以执行2条vaddpd
或vaddsd
指令(如果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 [=10=]x1,%rax
1094: 75 ea jne 1080
每个 vaddsd
取决于上一次迭代的结果,它必须等待 3 或 4 个循环才能执行。但是所有 vaddsd
和 sub
和 jne
的执行都可能在那段时间发生。因此,对于这个简单的循环,执行一个 vaddpd
或四个 vaddsd
.
要完全耗尽 vaddpd
指令,您需要执行其中的 6 或 8 个不依赖于彼此结果的指令(或者有其他指令做一些独立的工作)。
使用 AVX
; With avx
1070: c5 fd 58 c1 vaddpd %ymm1,%ymm0,%ymm0
1074: 48 83 e8 01 sub [=10=]x1,%rax
1078: 75 f6 jne 1070
此循环使用 ymm0
作为累加器。换句话说,它正在做 ymm0 += ymm1
(这是一个向量运算;一次添加 4 个双精度值)。因此它对 ymm0
具有循环携带依赖性(每个新添加都必须等待前一个添加完成并使用结果开始下一个添加)。 vaddpd
Zen+ 的延迟=3,吞吐量=1(根据https://www.uops.info/table.html)。循环携带的依赖性使得这个循环瓶颈在 vaddpd
的 latency 上,所以你的循环最多可以得到 3 cycles/iteration。在 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 [=11=]x1,%rax
1094: 75 ea jne 1080
这个循环将结果累加到 4 个不同的累加器中。基本上它在做:
xmm0 += xmm4
xmm1 += xmm5
xmm2 += xmm7
xmm3 += xmm6
所有这些加法彼此独立(并且它们是标量加法,因此每个加法仅对单个 64 位浮点值进行运算)。 vaddsd
延迟=3,吞吐量=0.5(每条指令的周期数)。这意味着它可以在一个周期内开始执行前 2 个加法。然后在下一个循环中它将开始第二对添加。因此,有可能根据吞吐量为该循环实现 2 cycles/iteration。但是延迟,正如你记得的那样是 3 个周期。所以这个循环在延迟上也有瓶颈。展开一次(使用 4 个额外的累加器;或者通过在将 xmm4-7 添加到主累加器之前在彼此之间添加 xmm4-7 来打破循环中的循环携带 dep.chain)以摆脱瓶颈(它可能会达到 ~50%更快)。
请注意,此(“无 AVX”)反汇编仍在使用 VEX 编码,因此技术上仍需要支持 AVX CPU。
关于基准测试
请注意,您的反汇编没有任何加载或存储,因此这可能代表也可能不代表添加 2 个 4 双精度向量数组的性能比较。