AVX2 和 AVX512 的加速

Pri*_*lab 0 c avx avx2 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;
}
Run Code Online (Sandbox Code Playgroud)

因此,代码分三阶段分发。存在三个阵列。这只是一个简单的数组加法。首先我们使用通用循环执行它,然后使用 AVX2,然后是 AVX 512。我使用的是 Intel Xeon 6130 处理器。

代码是使用命令编译的,

gcc -o test.o test.c -mavx512f -fopenmp -mavx2
Run Code Online (Sandbox Code Playgroud)

输出是,

General Time taken 0.532550
AVX2 Time taken 0.175549
AVX512 Time taken 0.264475
Run Code Online (Sandbox Code Playgroud)

现在,在通用循环和内在实现的情况下,加速是可见的。但是时间从 AVX2 增加到 AVX512,理论上不应该增加。

我已经检查过单独的加载、添加、存储操作。AVX512 的存储操作需要最大的时序。

只是为了检查我是否从两个代码段中删除了存储操作,结果时间是,

General Time taken 0.530248
AVX2 Time taken 0.115234
AVX512 Time taken 0.107062
Run Code Online (Sandbox Code Playgroud)

任何人都可以对这种行为有所了解还是预期?

********* 更新 1 *********

使用 -O3 -march=native 扩展编译后,新的时间是,

General Time taken 0.014887
AVX2 Time taken 0.008072
AVX512 Time taken 0.014630
Run Code Online (Sandbox Code Playgroud)

这些是所有加载、添加、存储指令。

********* 更新 2 *********

测试 1:

通用循环修改如下,

for(i=0;i<N;i++)
{
    //C[i] = A[i] + B[i];
    //G[i] = E[i] + F[i];
}
Run Code Online (Sandbox Code Playgroud)

输出是,

General Time taken 0.000003
AVX2 Time taken 0.014877
AVX512 Time taken 0.014334
Run Code Online (Sandbox Code Playgroud)

因此在这两种情况下都会发生页面错误

测试 2:

通用循环修改如下,

for(i=0;i<N;i++)
{
    C[i] = A[i] + B[i];
    G[i] = E[i] + F[i];
}
Run Code Online (Sandbox Code Playgroud)

因此,在这两种情况下都进行了缓存。

输出是,

General Time taken 0.029703
AVX2 Time taken 0.008500
AVX512 Time taken 0.008560
Run Code Online (Sandbox Code Playgroud)

测试 3:

在所有场景中都添加了一个虚拟外循环,并将N的大小减少到160000

for(j=0;j<N;j++)
{
    for(i=0;i<N;i+= /* 1 or 8 or 16 */)
    {
         // Code
    }
}
Run Code Online (Sandbox Code Playgroud)

现在输出是,

General Time taken 6.969532
AVX2 Time taken 0.871133
AVX512 Time taken 0.447317
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 5

您的 AVX2 测试重用了您已经在“通用”测试中编写的相同数组。所以它已经出现了页面错误。

您的 AVX512 测试正在写入尚未触及的数组,并且必须支付定时区域中这些页面错误的成本。要么在定时区域之外弄脏它,要么C[]再次重复使用。或者mmap(MAP_POPULATE)也可以连接可写页面。(对于实际使用,延迟页面错误可能会更好。让内核在您写入之前将几页归零可能会降低总成本,因为在内核的归零存储写回外部缓存之前让您的真实写入在 L1d 缓存中命中.)

请注意,“一般”时间(对于自动矢量化的第一个循环)几乎与“AVX512”时间相同。 (使用gcc -O3 -march=native,GCC 将使用 256 位向量自动向量化“通用”循环,根据-mprefer-vector-width=256for的默认调整-march=skylake-avx512)。

这些循环所做的工作基本相同:读取 2 个已初始化的数组并写入一个尚未触及的数组,从而导致页面错误。


使用 512 位向量(限制最大 turbo)导致的较低时钟速度不应大幅降低内存带宽。(使用这种 2 读 / 1 写访问模式,您将遇到内存瓶颈。)如果非核心(L3 / 网格)减慢以匹配最快的核心,则可能会减少一些带宽,但如果在所有。

这个类似 STREAM 的测试的内存带宽应该与 256 位向量和 512 位向量几乎相同。如果您想从 512 位向量中看到可测量的加速,以解决每个内存带宽计算量如此之少的问题,您将需要您的阵列适合 L1d 缓存并且已经很热了。或者可能是 L2 缓存。(在对数组进行迭代的内部循环周围使用重复循环,以便它可以运行足够长的时间以获得良好的计时精度)。AVX2 可以轻松地跟上 L3 或内存,因此 AVX512 不会帮助处理大数组,除非您为每个向量做更多的工作。


一旦启用优化 ( https://godbolt.org/z/w4zcrC),asm循环就没有什么奇怪的了,所以我不得不仔细看看你实际编写的数组。

甚至在 AVX2 循环运行之前,A 和 B 可能就已从缓存中完全驱逐(因为您N的文件太大了;对于、 和A,每个 662 MiB )。但是为 AVX2 和 AVX512 初始化不同的数组仍然有点奇怪,并且不运行任何预热循环以确保 CPU 处于最大涡轮增压。BC

“一般”时间基本上充当时钟速度和C[]阵列中页面故障的预热循环,因此测量的实际时间不会表示写入已经脏的内存的内存带宽。您或许可以使用它perf来查看在内核中花费了多少时间。