首页 > 解决方案 > 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;
}

因此,代码分三个阶段分发。存在三个阵列。这只是一个简单的数组加法。首先我们使用通用循环执行它,然后使用 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

标签: cavxavx2avx512

解决方案


您的 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的文件太大了;比如 、 和 各 662 AMiB BC。但是为 AVX2 和 AVX512 初始化不同的阵列仍然有点奇怪,并且不运行任何预热循环以确保 CPU 处于最大涡轮状态。

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


推荐阅读