【问题标题】:How are the gather instructions in AVX2 implemented?AVX2 中的收集指令是如何实现的?
【发布时间】:2014-03-13 12:35:23
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: intel ram simd avx avx2


    【解决方案1】:

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

    注意:这个答案现在已经过时了,因为自 Haswell 以来情况发生了很大变化。有关完整详细信息,请参阅已接受的答案(除非您碰巧针对的是 Haswell CPU)。

    【讨论】:

    • 看看 Agner 的表,好像是 20+ uops。所以是的,我不会称之为原生支持。看看 Skylake 做了什么会很有趣。可能更接近 GPU 的功能? (周期数 = 银行冲突数)
    • @PaulR,当数据在同一缓存行中时,收集可能有用吗?也许这对于将 SoA 转换为 Aos 而不必进行转置很有用(假设结构适合缓存行)。
    • 我已经用同一缓存行中的连续数据对其进行了测试,但没有看到任何好处 - 唯一的好处似乎是您不需要进行标量加载,然后将它们组合成一个向量。
    • 另一方面,我有一台装有 Skylake 芯片的新笔记本电脑。我找到了 Skylake 指令延迟/吞吐量列表。但他们缺乏收集指令。当我有时间时,我会尝试测试它。它可以作为 AVX512 收集/散射性能的前身。有一些非常有力的证据表明,桌面 Skylake 上的 SIMD 单元确实只是 AVX512 版本的一半宽度(其他一切都相同)。因此,我们在当前 Skylakes 上看到的任何内容都可能与未来的 AVX512 非常相似,如果不一样的话。
    • 从 Knights Landing AVX512 开始,聚集/分散仍然被分解为 uops。聚集以 2 个车道/周期运行,并以 1 个车道/周期分散。如此精确地匹配 2 load/1 store 端口架构。看起来 Skylake 是一样的。因此,与上一代相比的改进是消除了所有开销操作,只留下了原始内存访问。
    【解决方案2】:

    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
    

    从表中可以清楚地看出,在所有情况下,聚集负载都比标量负载快(对于我使用的基准)。

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

    英特尔手册显示了一些关于收集的不错的数据

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

    //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];
    }
    

    【讨论】:

    猜你喜欢
    • 2020-03-08
    • 2013-07-21
    • 2013-04-18
    • 1970-01-01
    • 2016-07-29
    • 1970-01-01
    • 2018-02-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多