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

Anu*_*lia 23 ram intel simd avx avx2

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

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

我读了几篇论述前者的文章(这是对我更有意义的文章),但我想更多地了解这一点.

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

Pau*_*l R 18

我做了一些AVX收集指令的基准测试,它似乎是一个相当简单的暴力实现 - 即使要加载的元素是连续的,似乎每个元素仍然有一个读取周期,所以性能实际上并不比仅仅更好做标量负荷.

  • 另一方面,我有一台新的笔记本电脑,里面装有Skylake芯片.我找到了Skylake指令延迟/吞吐量列表.但他们缺乏收集说明.当我有空的时候,我会尝试测试它.它可能是AVX512收集/分散性能的前兆.有一些非常有力的证据表明,桌面Skylake上的SIMD单元实际上只是AVX512版本的一半宽度(其他一切都是相同的).因此,无论我们在当前的Skylakes上看到什么,都可能与AVX512的未来相似,如果不相同的话. (5认同)
  • 从Knights Landing AVX512开始,聚集/散射仍然被分解为uops.收集以2个泳道/循环运行并以1个泳道/循环分散.因此精确匹配2个load/1存储端口架构.看起来Skylake是一样的.因此,对上一代的改进是消除了仅留下原始内存访问的所有开销操作. (5认同)
  • 我在相同的缓存行中使用连续数据测试它并且没有看到任何好处 - 唯一的好处似乎是你不需要做标量加载然后将它们组合成一个向量. (3认同)
  • 看看Agner的表,它就像是20多个uops.所以,是的,我不会称之为原生支持.看看Skylake的作用会很有趣.可能更接近GPU的作用?(周期数=银行冲突数#) (2认同)
  • @PaulR,当数据在同一个缓存行中时,聚集是否有用?也许这对于将SoA转换为Aos而不必进行转置(假设结构适合缓存行)非常有用. (2认同)
  • @PaulR:可能用于下一个CPU的未来优化.例如,看看自SSE1引入以来已经优化了多少未对齐的加载/存储,它实际上没有任何好处. (2认同)
  • 我最近不得不做一些需要真正聚集的东西.(即`data [index [i]]`).在Haswell上,4个索引加载+ 2x`movsd` + 2x`movhpd` +`vinsertf128`仍然*比ymm加载+`vgatherqpd`快得多.所以即使在最好的情况下,4路聚集仍然会失败.我没有试过8路聚会. (2认同)
  • Knights Corner进行了缓存行合并。因此,属于同一高速缓存行的多个访问将通过AVX512的64字节宽端口作为一次访问进入。然后在加载/存储单元内,它将进行必要的改组/合并以使其实现。显然他们是从KNL那里拿出来的。这可能太复杂了。如果我们禁止泳道冲突在同一周期的同一端口上执行,那么我*相信*可以在同一周期的多个泳道上重用某些每端口复用逻辑。但这还需要冲突检测逻辑。(AVX512具有...) (2认同)

Z b*_*son 6

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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];
}
Run Code Online (Sandbox Code Playgroud)