AVX2 和 AVX512 加速
Speedup by AVX2 and AVX512
我正在尝试可视化合并 AVX2 和 AVX512 的加速
#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>
#include <omp.h>
#include <time.h>
int main()
{
long i, N = 160000000;
int * A = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);
int * B = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);
int * C = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);
int * E = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);
int * F = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);
int * G = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);
srand(time(0));
for(i=0;i<N;i++)
{
A[i] = rand();
B[i] = rand();
E[i] = rand();
F[i] = rand();
}
double time = omp_get_wtime();
for(i=0;i<N;i++)
{
C[i] = A[i] + B[i];
}
time = omp_get_wtime() - time;
printf("General Time taken %lf\n", time);
__m256i A_256_VEC, B_256_VEC, C_256_VEC;
time = omp_get_wtime();
for(i=0;i<N;i+=8)
{
A_256_VEC = _mm256_load_si256((__m256i *)&A[i]);
B_256_VEC = _mm256_load_si256((__m256i *)&B[i]);
C_256_VEC = _mm256_add_epi32(A_256_VEC, B_256_VEC);
_mm256_store_si256((__m256i *)&C[i],C_256_VEC);
}
time = omp_get_wtime() - time;
printf("AVX2 Time taken %lf\n", time);
free(A);
free(B);
free(C);
__m512i A_512_VEC, B_512_VEC, C_512_VEC;
time = omp_get_wtime();
for(i=0;i<N;i+=16)
{
A_512_VEC = _mm512_load_si512((__m512i *)&E[i]);
B_512_VEC = _mm512_load_si512((__m512i *)&F[i]);
C_512_VEC = _mm512_add_epi32(A_512_VEC, B_512_VEC);
_mm512_store_si512((__m512i *)&G[i],C_512_VEC);
}
time = omp_get_wtime() - time;
printf("AVX512 Time taken %lf\n", time);
for(i=0;i<N;i++)
{
if(G[i] != E[i] + F[i])
{
printf("Not Matched !!!\n");
break;
}
}
free(E);
free(F);
free(G);
return 1;
}
因此,代码分三个阶段分发。存在三个阵列。这只是一个简单的数组加法。首先我们使用通用循环执行它,然后使用 AVX2,然后是 AVX 512。我使用的是 Intel Xeon 6130 处理器。
代码是使用命令编译的,
gcc -o test.o test.c -mavx512f -fopenmp -mavx2
输出是,
General Time taken 0.532550
AVX2 Time taken 0.175549
AVX512 Time taken 0.264475
现在,在一般循环和内部实现的情况下,加速是可见的。但是时间从AVX2增加到AVX512,理论上不应该。
我检查了单独的加载、添加、存储操作。 AVX512的存储操作占用的时间最多。
只是为了检查我是否从两个代码段中删除了存储操作,结果时间是,
General Time taken 0.530248
AVX2 Time taken 0.115234
AVX512 Time taken 0.107062
任何人都可以对这种行为有所了解,或者这是意料之中的事情吗?
********* 更新 1 *********
使用 -O3 -march=native 扩展编译后,新的时间是,
General Time taken 0.014887
AVX2 Time taken 0.008072
AVX512 Time taken 0.014630
这些包含所有加载、添加、存储指令。
********* 更新 2 *********
测试 1:
通用循环修改如下,
for(i=0;i<N;i++)
{
//C[i] = A[i] + B[i];
//G[i] = E[i] + F[i];
}
输出是,
General Time taken 0.000003
AVX2 Time taken 0.014877
AVX512 Time taken 0.014334
因此在这两种情况下都发生了页面错误
测试 2:
通用循环修改如下,
for(i=0;i<N;i++)
{
C[i] = A[i] + B[i];
G[i] = E[i] + F[i];
}
因此,在这两种情况下都进行了缓存。
输出是,
General Time taken 0.029703
AVX2 Time taken 0.008500
AVX512 Time taken 0.008560
测试 3:
在所有场景中都添加了一个虚拟外循环,N的大小减少到160000.
for(j=0;j<N;j++)
{
for(i=0;i<N;i+= /* 1 or 8 or 16 */)
{
// Code
}
}
现在输出是,
General Time taken 6.969532
AVX2 Time taken 0.871133
AVX512 Time taken 0.447317
您的 AVX2 测试重用了您已经用 "general" 测试编写的相同数组。所以它已经出现页面错误了。
您的 AVX512 测试正在写入一个尚未被触及的数组,并且必须支付计时区域中那些页面错误的成本。要么在定时区域之外弄脏它,要么再次重复使用 C[]
。或者 mmap(MAP_POPULATE)
也可以,连接可写页面。 (对于现实世界的使用,惰性页面错误可能更好。让内核在您写入它们之前将几页归零可能会降低总成本,因为在内核的归零存储写回外部缓存之前让您的实际写入命中 L1d 缓存.)
请注意 "general" 时间(对于自动向量化的第一个循环)几乎与 "AVX512" 时间相同。(gcc -O3 -march=native
,GCC 将使用 256 位向量自动向量化 "general" 循环,根据 -march=skylake-avx512
的 -mprefer-vector-width=256
的默认调整)。
这些循环基本上做着相同的工作:读取 2 个已初始化的数组并写入一个尚未触及的数组,导致页面错误。
使用 512 位向量(限制最大 turbo)的较低时钟速度不会降低内存带宽太多。 (你将使用这种 2 读/1 写访问模式在内存上形成瓶颈。)如果非核心(L3 / 网格)减慢速度以匹配最快的核心,那可能会减少一些带宽,但如果完全存在。
这个类似 STREAM 的测试的内存带宽应该与 256 和 512 位向量几乎相同。如果你想看到 512 位向量的可测量加速,以解决每个内存带宽计算量如此之少的问题,你将需要你的数组适合 L1d 缓存并且已经很热了。或者可能是 L2 缓存。 (在遍历数组的内部循环周围使用重复循环,这样它就可以 运行 足够长的时间以获得良好的计时精度)。为此,AVX2 可以很容易地跟上 L3 或内存,因此 AVX512 对大数组没有帮助,除非你在每个向量上做更多的工作。
一旦启用优化 (https://godbolt.org/z/w4zcrC),asm 循环就没有什么奇怪的了,所以我不得不仔细看看您实际编写的数组。
A 和 B 可能甚至在 AVX2 循环 运行s 之前就已完全从缓存中逐出(因为您的 N
太大;例如 A
每个 662 MiB,B
,和 C
)。但是为 AVX2 和 AVX512 初始化不同的数组,而不是 运行 任何预热循环以确保 CPU 处于最大涡轮增压,这仍然有点奇怪。
"general" 时间基本上作为 C[]
数组中时钟速度和页面错误的预热循环,因此实际测量的时间不具有指示性用于写入已经脏的内存的内存带宽。您也许可以使用 perf
查看内核花费了多少时间。
我正在尝试可视化合并 AVX2 和 AVX512 的加速
#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>
#include <omp.h>
#include <time.h>
int main()
{
long i, N = 160000000;
int * A = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);
int * B = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);
int * C = (int *)aligned_alloc(sizeof(__m256), sizeof(int) * N);
int * E = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);
int * F = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);
int * G = (int *)aligned_alloc(sizeof(__m512), sizeof(int) * N);
srand(time(0));
for(i=0;i<N;i++)
{
A[i] = rand();
B[i] = rand();
E[i] = rand();
F[i] = rand();
}
double time = omp_get_wtime();
for(i=0;i<N;i++)
{
C[i] = A[i] + B[i];
}
time = omp_get_wtime() - time;
printf("General Time taken %lf\n", time);
__m256i A_256_VEC, B_256_VEC, C_256_VEC;
time = omp_get_wtime();
for(i=0;i<N;i+=8)
{
A_256_VEC = _mm256_load_si256((__m256i *)&A[i]);
B_256_VEC = _mm256_load_si256((__m256i *)&B[i]);
C_256_VEC = _mm256_add_epi32(A_256_VEC, B_256_VEC);
_mm256_store_si256((__m256i *)&C[i],C_256_VEC);
}
time = omp_get_wtime() - time;
printf("AVX2 Time taken %lf\n", time);
free(A);
free(B);
free(C);
__m512i A_512_VEC, B_512_VEC, C_512_VEC;
time = omp_get_wtime();
for(i=0;i<N;i+=16)
{
A_512_VEC = _mm512_load_si512((__m512i *)&E[i]);
B_512_VEC = _mm512_load_si512((__m512i *)&F[i]);
C_512_VEC = _mm512_add_epi32(A_512_VEC, B_512_VEC);
_mm512_store_si512((__m512i *)&G[i],C_512_VEC);
}
time = omp_get_wtime() - time;
printf("AVX512 Time taken %lf\n", time);
for(i=0;i<N;i++)
{
if(G[i] != E[i] + F[i])
{
printf("Not Matched !!!\n");
break;
}
}
free(E);
free(F);
free(G);
return 1;
}
因此,代码分三个阶段分发。存在三个阵列。这只是一个简单的数组加法。首先我们使用通用循环执行它,然后使用 AVX2,然后是 AVX 512。我使用的是 Intel Xeon 6130 处理器。
代码是使用命令编译的,
gcc -o test.o test.c -mavx512f -fopenmp -mavx2
输出是,
General Time taken 0.532550
AVX2 Time taken 0.175549
AVX512 Time taken 0.264475
现在,在一般循环和内部实现的情况下,加速是可见的。但是时间从AVX2增加到AVX512,理论上不应该。
我检查了单独的加载、添加、存储操作。 AVX512的存储操作占用的时间最多。
只是为了检查我是否从两个代码段中删除了存储操作,结果时间是,
General Time taken 0.530248
AVX2 Time taken 0.115234
AVX512 Time taken 0.107062
任何人都可以对这种行为有所了解,或者这是意料之中的事情吗?
********* 更新 1 *********
使用 -O3 -march=native 扩展编译后,新的时间是,
General Time taken 0.014887
AVX2 Time taken 0.008072
AVX512 Time taken 0.014630
这些包含所有加载、添加、存储指令。
********* 更新 2 *********
测试 1:
通用循环修改如下,
for(i=0;i<N;i++)
{
//C[i] = A[i] + B[i];
//G[i] = E[i] + F[i];
}
输出是,
General Time taken 0.000003
AVX2 Time taken 0.014877
AVX512 Time taken 0.014334
因此在这两种情况下都发生了页面错误
测试 2:
通用循环修改如下,
for(i=0;i<N;i++)
{
C[i] = A[i] + B[i];
G[i] = E[i] + F[i];
}
因此,在这两种情况下都进行了缓存。
输出是,
General Time taken 0.029703
AVX2 Time taken 0.008500
AVX512 Time taken 0.008560
测试 3:
在所有场景中都添加了一个虚拟外循环,N的大小减少到160000.
for(j=0;j<N;j++)
{
for(i=0;i<N;i+= /* 1 or 8 or 16 */)
{
// Code
}
}
现在输出是,
General Time taken 6.969532
AVX2 Time taken 0.871133
AVX512 Time taken 0.447317
您的 AVX2 测试重用了您已经用 "general" 测试编写的相同数组。所以它已经出现页面错误了。
您的 AVX512 测试正在写入一个尚未被触及的数组,并且必须支付计时区域中那些页面错误的成本。要么在定时区域之外弄脏它,要么再次重复使用 C[]
。或者 mmap(MAP_POPULATE)
也可以,连接可写页面。 (对于现实世界的使用,惰性页面错误可能更好。让内核在您写入它们之前将几页归零可能会降低总成本,因为在内核的归零存储写回外部缓存之前让您的实际写入命中 L1d 缓存.)
请注意 "general" 时间(对于自动向量化的第一个循环)几乎与 "AVX512" 时间相同。(gcc -O3 -march=native
,GCC 将使用 256 位向量自动向量化 "general" 循环,根据 -march=skylake-avx512
的 -mprefer-vector-width=256
的默认调整)。
这些循环基本上做着相同的工作:读取 2 个已初始化的数组并写入一个尚未触及的数组,导致页面错误。
使用 512 位向量(限制最大 turbo)的较低时钟速度不会降低内存带宽太多。 (你将使用这种 2 读/1 写访问模式在内存上形成瓶颈。)如果非核心(L3 / 网格)减慢速度以匹配最快的核心,那可能会减少一些带宽,但如果完全存在。
这个类似 STREAM 的测试的内存带宽应该与 256 和 512 位向量几乎相同。如果你想看到 512 位向量的可测量加速,以解决每个内存带宽计算量如此之少的问题,你将需要你的数组适合 L1d 缓存并且已经很热了。或者可能是 L2 缓存。 (在遍历数组的内部循环周围使用重复循环,这样它就可以 运行 足够长的时间以获得良好的计时精度)。为此,AVX2 可以很容易地跟上 L3 或内存,因此 AVX512 对大数组没有帮助,除非你在每个向量上做更多的工作。
一旦启用优化 (https://godbolt.org/z/w4zcrC),asm 循环就没有什么奇怪的了,所以我不得不仔细看看您实际编写的数组。
A 和 B 可能甚至在 AVX2 循环 运行s 之前就已完全从缓存中逐出(因为您的 N
太大;例如 A
每个 662 MiB,B
,和 C
)。但是为 AVX2 和 AVX512 初始化不同的数组,而不是 运行 任何预热循环以确保 CPU 处于最大涡轮增压,这仍然有点奇怪。
"general" 时间基本上作为 C[]
数组中时钟速度和页面错误的预热循环,因此实际测量的时间不具有指示性用于写入已经脏的内存的内存带宽。您也许可以使用 perf
查看内核花费了多少时间。