我正在尝试可视化将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,然后是AVX512。我正在使用英特尔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发布于 2020-02-04 06:50:31
您的AVX2测试重用了已经用“通用”测试编写的数组。所以已经有页错了。
您的AVX512测试正在写入一个尚未被触摸的数组,并且必须支付时间区域中这些页面结果的成本。要么将其移到定时区域之外,要么再次重用C[]。或者mmap(MAP_POPULATE)也能工作,连接可写的页面。(对于现实世界的使用来说,懒惰的页面错误可能更好。让内核在编写前几页为零可能会降低总成本,因为在内核的零存储写回外部缓存之前,可以让真正的写操作在L1d缓存中命中。
注意到,“通用”时间(用于自动向量化的第一个循环)几乎与"AVX512“时间相同。(与gcc -O3 -march=native一起,GCC将使用256位向量自动向量化”通用“循环,这是-mprefer-vector-width=256 for -march=skylake-avx512的默认调优)。
这些循环所做的工作基本上是一样的:读取2个初始化数组,并编写一个尚未触及的数组,从而导致页面错误。
较低的时钟速度使用512位矢量(限制最大涡轮)不应该大大降低内存带宽。(这种2读/1写访问模式将导致内存瓶颈。)如果解核(L3 / mesh)速度减慢,以匹配最快的核心,这可能会减少一些带宽,但如果存在的话,效果似乎很小。
这种类似流的测试的内存带宽应该与256 vs512位矢量的内存带宽基本相同。如果您想看到从512位向量中可以测量到的加速比,对于每个内存带宽计算如此之少的问题,您将需要您的数组来适应L1d缓存,并且已经很热了。或者可能是L2缓存。(在对数组进行迭代的内环周围使用一个重复循环,这样就可以运行足够长的时间,从而达到良好的计时精度)。AVX2可以很容易地与L3或内存保持同步,这样AVX512就不会帮助处理大数组,除非您在每个向量上做更多的工作。
启用优化(https://godbolt.org/z/w4zcrC)后,asm循环没有什么奇怪之处,所以我不得不仔细看看您实际编写的数组。
甚至在AVX2循环运行之前,A和B就可能被从缓存中完全逐出(因为您的N太大了;A、B和C各有662个MiB )。但是,为AVX2和AVX512插入不同的数组,而不运行任何热身循环以确保CPU处于最大涡轮时,这仍然有点奇怪。
“一般”时间基本上是C[]数组中时钟速度和页面错误的热身循环,因此它所测量的实际时间并不表示写入已经脏的内存的内存带宽。您可以使用perf查看在内核中花费了多少时间。
https://stackoverflow.com/questions/60051453
复制相似问题