代码之家  ›  专栏  ›  技术社区  ›  Anuj Kalia

AVX2中的收集指令是如何实现的?

  •  26
  • Anuj Kalia  · 技术社区  · 11 年前

    假设我使用的是AVX2的VGATHERDPS-这应该使用8个DWORD索引加载8个单精度浮点。

    当要加载的数据存在于不同的缓存行中时会发生什么?指令是否实现为一个硬件循环,逐个获取缓存行?或者,它可以一次向多个缓存行发出加载吗?

    我读了几篇关于前者的论文(这对我来说更有意义),但我想了解更多。

    链接到一篇论文: http://arxiv.org/pdf/1401.7494.pdf

    2 回复  |  直到 11 年前
        1
  •  19
  •   Paul R    5 年前

    我对AVX收集指令(在Haswell CPU上)进行了一些基准测试,这似乎是一个相当简单的暴力实现——即使要加载的元素是连续的,每个元素似乎仍有一个读取周期,因此性能实际上并不比只进行标量加载好。

    注:这个答案现在已经过时了,因为自从哈斯韦尔以来,情况发生了很大变化。有关完整的详细信息,请参阅公认的答案(除非您恰好针对Haswell CPU)。

        2
  •  12
  •   Peter Cordes    5 年前

    Gather最初是用Haswell实现的,但直到Broadwell(Haswell之后的第一代)才被优化。

    我编写了自己的代码来测试聚集(见下文)。以下是Skylake、SkylakeX(具有专用AVX512端口)和KNL系统的概述。

                     scalar    auto   AVX2   AVX512
    Skylake GCC        0.47    0.38   0.38       NA
    SkylakeX GCC       0.56    0.23   0.35     0.24
    KNL GCC            3.95    1.37   2.11     1.16
    KNL ICC            3.92    1.17   2.31     1.17
    

    从表中可以明显看出,在所有情况下,聚集加载都比标量加载快(对于我使用的基准测试)。

    我不确定Intel如何实现内部收集。面具似乎对聚集的性能没有影响。这是Intel可以优化的一件事(如果您只读取一个标量值以应付掩码,那么它应该比收集所有值然后使用掩码更快。

    英特尔手册显示了一些不错的数据

    https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf
    DCU=一级数据缓存单元。MCU=中级=二级缓存。LLC=最后一级=L3缓存。L3是共享的,L2和L1d是每个核心专用的。
    英特尔是 只是 基准测试收集,而不是将结果用于任何事情。

    enter image description here enter image description here

    //gather.c
    #include <stdio.h>
    #include <omp.h>
    #include <stdlib.h>
    
    #define N 1024
    #define R 1000000
    
    void foo_auto(double * restrict a, double * restrict b, int *idx, int n);
    void foo_AVX2(double * restrict a, double * restrict b, int *idx, int n);
    void foo_AVX512(double * restrict a, double * restrict b, int *idx, int n);
    void foo1(double * restrict a, double * restrict b, int *idx, int n);
    void foo2(double * restrict a, double * restrict b, int *idx, int n);
    void foo3(double * restrict a, double * restrict b, int *idx, int n);
    
    
    double test(int *idx, void (*fp)(double * restrict a, double * restrict b, int *idx, int n)) {
      double a[N];
      double b[N];
      double dtime;
    
      for(int i=0; i<N; i++) a[i] = 1.0*N;
      for(int i=0; i<N; i++) b[i] = 1.0;
      fp(a, b, idx, N);
      dtime = -omp_get_wtime();
      for(int i=0; i<R; i++) fp(a, b, idx, N);
      dtime += omp_get_wtime();
      return dtime;
    }
    
    int main(void) {
    
      //for(int i=0; i<N; i++) idx[i] = N - i - 1;
      //for(int i=0; i<N; i++) idx[i] = i;
      //for(int i=0; i<N; i++) idx[i] = rand()%N;
    
      //for(int i=0; i<R; i++) foo2(a, b, idx, N);
      int idx[N];
      double dtime;
      int ntests=2;
      void (*fp[4])(double * restrict a, double * restrict b, int *idx, int n);
      fp[0] = foo_auto;
      fp[1] = foo_AVX2;
    #if defined ( __AVX512F__ ) || defined ( __AVX512__ )
      fp[2] = foo_AVX512;
      ntests=3;
    #endif     
    
      for(int i=0; i<ntests; i++) { 
        for(int i=0; i<N; i++) idx[i] = 0;
        test(idx, fp[i]);
        dtime = test(idx, fp[i]);
        printf("%.2f      ", dtime);
    
        for(int i=0; i<N; i++) idx[i] = i;
        test(idx, fp[i]);
        dtime = test(idx, fp[i]);
        printf("%.2f      ", dtime);
    
        for(int i=0; i<N; i++) idx[i] = N-i-1;
        test(idx, fp[i]);
        dtime = test(idx, fp[i]);
        printf("%.2f      ", dtime);
    
        for(int i=0; i<N; i++) idx[i] = rand()%N;
        test(idx, fp[i]);
        dtime = test(idx, fp[i]);
        printf("%.2f\n", dtime);
      }
    
      for(int i=0; i<N; i++) idx[i] = 0;
      test(idx, foo1);
      dtime = test(idx, foo1);
      printf("%.2f      ", dtime);
    
      for(int i=0; i<N; i++) idx[i] = i;
      test(idx, foo2);
      dtime = test(idx, foo2);
      printf("%.2f      ", dtime);
    
      for(int i=0; i<N; i++) idx[i] = N-i-1;
      test(idx, foo3);
      dtime = test(idx, foo3);
      printf("%.2f      ", dtime);
      printf("NA\n");
    }
    
    //foo2.c
    #include <x86intrin.h>
    void foo_auto(double * restrict a, double * restrict b, int *idx, int n) {
      for(int i=0; i<n; i++) b[i] = a[idx[i]];
    }
    
    void foo_AVX2(double * restrict a, double * restrict b, int *idx, int n) {
      for(int i=0; i<n; i+=4) {
        __m128i vidx = _mm_loadu_si128((__m128i*)&idx[i]);
        __m256d av = _mm256_i32gather_pd(&a[i], vidx, 8);
        _mm256_storeu_pd(&b[i],av);
      }
    }
    
    #if defined ( __AVX512F__ ) || defined ( __AVX512__ )
    void foo_AVX512(double * restrict a, double * restrict b, int *idx, int n) {
      for(int i=0; i<n; i+=8) {
        __m256i vidx = _mm256_loadu_si256((__m256i*)&idx[i]);
        __m512d av = _mm512_i32gather_pd(vidx, &a[i], 8);
        _mm512_storeu_pd(&b[i],av);
      }
    }
    #endif
    
    void foo1(double * restrict a, double * restrict b, int *idx, int n) {
      for(int i=0; i<n; i++) b[i] = a[0];
    }
    
    void foo2(double * restrict a, double * restrict b, int *idx, int n) {
      for(int i=0; i<n; i++) b[i] = a[i];
    }
    
    void foo3(double * restrict a, double * restrict b, int *idx, int n) {
      for(int i=0; i<n; i++) b[i] = a[n-i-1];
    }