假设我正在使用AVX2的VGATHERDPS - 这应该使用8个DWORD索引加载8个单精度浮点数。
当要加载的数据存在于不同的缓存行中时会发生什么?指令是作为硬件循环实现的,它逐个获取缓存行吗?或者,它是否可以立即向多个缓存行发出负载?
我读了几篇说明前者的文章(这是对我更有意义的文章),但我想更多地了解这一点。
答案 0 :(得分:18)
我做了一些AVX收集指令的基准测试,它似乎是一个相当简单的暴力实现 - 即使要加载的元素是连续的,似乎每个元素仍然有一个读周期,所以性能真的没有不仅仅是做标量负载。
答案 1 :(得分:3)
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可以优化的一件事(如果只读取一个标量值是由于使用了掩码,那么它应该比收集所有值然后使用掩码更快。
英特尔手册显示了一些不错的数字
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];
}