L1内存带宽:使用相差4096 + 64字节的地址,效率下降50%

我想用英特尔处理器实现以下操作的最大带宽。

for(int i=0; i<n; i++) z[i] = x[i] + y[i]; //n=2048 

其中x,y和z是浮点数组。 我在Haswell,Ivy Bridge和Westmere系统上这样做。

我最初分配了这样的内存

 char *a = (char*)_mm_malloc(sizeof(float)*n, 64); char *b = (char*)_mm_malloc(sizeof(float)*n, 64); char *c = (char*)_mm_malloc(sizeof(float)*n, 64); float *x = (float*)a; float *y = (float*)b; float *z = (float*)c; 

当我这样做时,我获得了每个系统预期的峰值带宽的大约50%。

峰值计算为frequency * average bytes/clock_cycle 。 每个系统的平均字节/时钟周期为:

 Core2: two 16 byte reads one 16 byte write per 2 clock cycles -> 24 bytes/clock cycle SB/IB: two 32 byte reads and one 32 byte write per 2 clock cycles -> 48 bytes/clock cycle Haswell: two 32 byte reads and one 32 byte write per clock cycle -> 96 bytes/clock cycle 

这意味着例如Haswell II仅观察48个字节/时钟周期(可能是一个时钟周期内的两次读取,另一次写入下一个时钟周期)。

我打印出bacb的地址差异,每个都是8256字节。 值8256是8192 + 64。 因此它们每个都比一个缓存行大一些数组大小(8192字节)。

一时兴起,我尝试像这样分配内存。

 const int k = 0; char *mem = (char*)_mm_malloc(1<<18,4096); char *a = mem; char *b = a+n*sizeof(float)+k*64; char *c = b+n*sizeof(float)+k*64; float *x = (float*)a; float *y = (float*)b; float *z = (float*)c; 

这几乎使我的峰值带宽增加了一倍,因此我现在可以获得90%的峰值带宽。 然而,当我尝试k=1它回落到50%。 我已经尝试了k其他值并且发现例如k=2k=33k=65仅获得峰的50%但是例如k=10k=32k=63给出全速。 我不明白这一点。

在Agner Fog的micrarchitecture手册中,他说存在与存储器地址的错误依赖关系,具有相同的设置和偏移

不能同时从间隔4 KB的地址读取和写入。

但这正是我看到最大利益的地方! 当k=0 ,存储器地址恰好相差2*4096字节。 Agner还谈到了缓存库冲突。 但Haswell和Westmere并不认为存在这些银行冲突,所以不应该解释我所观察到的。 这是怎么回事!?

据我所知,OoO执行决定读写哪个地址,即使数组的内存地址恰好相差4096字节,也不一定意味着处理器同时读取&x[0]和写入&z[0]但是那么为什么被单个缓存线关闭导致它窒息?

编辑:根据Evgeny Kluev的回答,我现在相信这就是Agner Fog所谓的“虚假商店转发摊位”。 在Pentium Pro,II和II的手册中,他写道:

有趣的是,如果在不同的缓存库中碰巧具有相同的设置值,那么在编写和读取完全不同的地址时,您可以获得一个伪造商店转发停顿:

 ; Example 5.28. Bogus store-to-load forwarding stall mov byte ptr [esi], al mov ebx, dword ptr [esi+4092] ; No stall mov ecx, dword ptr [esi+4096] ; Bogus stall 

编辑:这是k=0k=1每个系统的效率表。

  k=0 k=1 Westmere: 99% 66% Ivy Bridge: 98% 44% Haswell: 90% 49% 

我想我可以解释这些数字,如果我假设对于k=1 ,写入和读取不会发生在同一个时钟周期。

  cycle Westmere Ivy Bridge Haswell 1 read 16 read 16 read 16 read 32 read 32 2 write 16 read 16 read 16 write 32 3 write 16 4 write 16 k=1/k=0 peak 16/24=66% 24/48=50% 48/96=50% 

这个理论非常有效。 常春藤桥比我预期的要低一些,但Ivy Bridge遭遇银行缓存冲突,而其他人没有,所以这可能是另一个需要考虑的效果。

下面是工作代码来自己测试一下。 在没有AVX的系统上使用g++ -O3 sum.cpp编译,否则使用g++ -O3 -mavx sum.cpp编译。 尝试改变值k

 //sum.cpp #include  #include  #include  #include  #define TIMER_TYPE CLOCK_REALTIME double time_diff(timespec start, timespec end) { timespec temp; if ((end.tv_nsec-start.tv_nsec)<0) { temp.tv_sec = end.tv_sec-start.tv_sec-1; temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec; } else { temp.tv_sec = end.tv_sec-start.tv_sec; temp.tv_nsec = end.tv_nsec-start.tv_nsec; } return (double)temp.tv_sec + (double)temp.tv_nsec*1E-9; } void sum(float * __restrict x, float * __restrict y, float * __restrict z, const int n) { #if defined(__GNUC__) x = (float*)__builtin_assume_aligned (x, 64); y = (float*)__builtin_assume_aligned (y, 64); z = (float*)__builtin_assume_aligned (z, 64); #endif for(int i=0; i<n; i++) { z[i] = x[i] + y[i]; } } #if (defined(__AVX__)) void sum_avx(float *x, float *y, float *z, const int n) { float *x1 = x; float *y1 = y; float *z1 = z; for(int i=0; i<n/64; i++) { //unroll eight times _mm256_store_ps(z1+64*i+ 0,_mm256_add_ps(_mm256_load_ps(x1+64*i+ 0), _mm256_load_ps(y1+64*i+ 0))); _mm256_store_ps(z1+64*i+ 8,_mm256_add_ps(_mm256_load_ps(x1+64*i+ 8), _mm256_load_ps(y1+64*i+ 8))); _mm256_store_ps(z1+64*i+ 16,_mm256_add_ps(_mm256_load_ps(x1+64*i+16), _mm256_load_ps(y1+64*i+ 16))); _mm256_store_ps(z1+64*i+ 24,_mm256_add_ps(_mm256_load_ps(x1+64*i+24), _mm256_load_ps(y1+64*i+ 24))); _mm256_store_ps(z1+64*i+ 32,_mm256_add_ps(_mm256_load_ps(x1+64*i+32), _mm256_load_ps(y1+64*i+ 32))); _mm256_store_ps(z1+64*i+ 40,_mm256_add_ps(_mm256_load_ps(x1+64*i+40), _mm256_load_ps(y1+64*i+ 40))); _mm256_store_ps(z1+64*i+ 48,_mm256_add_ps(_mm256_load_ps(x1+64*i+48), _mm256_load_ps(y1+64*i+ 48))); _mm256_store_ps(z1+64*i+ 56,_mm256_add_ps(_mm256_load_ps(x1+64*i+56), _mm256_load_ps(y1+64*i+ 56))); } } #else void sum_sse(float *x, float *y, float *z, const int n) { float *x1 = x; float *y1 = y; float *z1 = z; for(int i=0; i<n/32; i++) { //unroll eight times _mm_store_ps(z1+32*i+ 0,_mm_add_ps(_mm_load_ps(x1+32*i+ 0), _mm_load_ps(y1+32*i+ 0))); _mm_store_ps(z1+32*i+ 4,_mm_add_ps(_mm_load_ps(x1+32*i+ 4), _mm_load_ps(y1+32*i+ 4))); _mm_store_ps(z1+32*i+ 8,_mm_add_ps(_mm_load_ps(x1+32*i+ 8), _mm_load_ps(y1+32*i+ 8))); _mm_store_ps(z1+32*i+ 12,_mm_add_ps(_mm_load_ps(x1+32*i+12), _mm_load_ps(y1+32*i+ 12))); _mm_store_ps(z1+32*i+ 16,_mm_add_ps(_mm_load_ps(x1+32*i+16), _mm_load_ps(y1+32*i+ 16))); _mm_store_ps(z1+32*i+ 20,_mm_add_ps(_mm_load_ps(x1+32*i+20), _mm_load_ps(y1+32*i+ 20))); _mm_store_ps(z1+32*i+ 24,_mm_add_ps(_mm_load_ps(x1+32*i+24), _mm_load_ps(y1+32*i+ 24))); _mm_store_ps(z1+32*i+ 28,_mm_add_ps(_mm_load_ps(x1+32*i+28), _mm_load_ps(y1+32*i+ 28))); } } #endif int main () { const int n = 2048; const int k = 0; float *z2 = (float*)_mm_malloc(sizeof(float)*n, 64); char *mem = (char*)_mm_malloc(1<<18,4096); char *a = mem; char *b = a+n*sizeof(float)+k*64; char *c = b+n*sizeof(float)+k*64; float *x = (float*)a; float *y = (float*)b; float *z = (float*)c; printf("x %p, y %p, z %p, yx %d, zy %d\n", a, b, c, ba, cb); for(int i=0; i<n; i++) { x[i] = (1.0f*i+1.0f); y[i] = (1.0f*i+1.0f); z[i] = 0; } int repeat = 1000000; timespec time1, time2; sum(x,y,z,n); #if (defined(__AVX__)) sum_avx(x,y,z2,n); #else sum_sse(x,y,z2,n); #endif printf("error: %d\n", memcmp(z,z2,sizeof(float)*n)); while(1) { clock_gettime(TIMER_TYPE, &time1); #if (defined(__AVX__)) for(int r=0; r<repeat; r++) sum_avx(x,y,z,n); #else for(int r=0; r<repeat; r++) sum_sse(x,y,z,n); #endif clock_gettime(TIMER_TYPE, &time2); double dtime = time_diff(time1,time2); double peak = 1.3*96; //haswell @1.3GHz //double peak = 3.6*48; //Ivy Bridge @ 3.6Ghz //double peak = 2.4*24; // Westmere @ 2.4GHz double rate = 3.0*1E-9*sizeof(float)*n*repeat/dtime; printf("dtime %f, %f GB/s, peak, %f, efficiency %f%%\n", dtime, rate, peak, 100*rate/peak); } } 

我认为ab之间的差距并不重要。 在bc之间只留下一个间隙后,我在Haswell上得到了以下结果:

 k % ----- 1 48 2 48 3 48 4 48 5 46 6 53 7 59 8 67 9 73 10 81 11 85 12 87 13 87 ... 0 86 

由于Haswell被认为没有银行冲突,唯一剩下的解释是内存地址之间的错误依赖(你已经在Agner Fog的微体系结构手册中找到了解释这个问题的适当位置)。 银行冲突与错误共享之间的区别在于,银行冲突阻止在同一时钟周期内两次访问同一银行,而虚假共享则阻止在您写入相同的偏移量之后读取4K内存中的某些偏移量(并且不仅仅是在相同的时钟周期内,也可以在写入后的几个时钟周期内)。

由于您的代码(对于k=0从相同偏移执行两次读取之后写入任何偏移量并且在很长时间内不会从中读取,因此这种情况应该被视为“最佳”,因此我将k=0在表的末尾。 对于k=1您总是从最近被覆盖的偏移读取,这意味着错误共享,从而降低性能。 随着写入和读取之间的k时间增加,CPU内核有更多机会将写入数据传递到所有内存层次结构(这意味着两个地址转换用于读取和写入,更新缓存数据和标记以及从缓存中获取数据,核心之间的数据同步,可能还有更多的东西)。 k=12或24个时钟(在我的CPU上)足以让每个写入的数据准备好进行后续的读操作,因此从这个值开始,性能将恢复正常。 看起来与AMD的20多个时钟没有太大区别(正如@Mysticial所说)。