C L1内存带宽:使用相差4096+;64字节
我希望在英特尔处理器上实现以下操作的最大带宽C L1内存带宽:使用相差4096+;64字节,c,caching,memory,x86,avx,C,Caching,Memory,X86,Avx,我希望在英特尔处理器上实现以下操作的最大带宽 for(int i=0; i<n; i++) z[i] = x[i] + y[i]; //n=2048 当我这样做时,我得到了每个系统预期峰值带宽的50% 峰值计算为频率*平均字节/时钟周期。每个系统的平均字节/时钟周期为: Core2: two 16 byte reads one 16 byte write per 2 clock cycles -> 24 bytes/clock cycle SB/IB: two 32 byt
for(int i=0; i<n; i++) z[i] = x[i] + y[i]; //n=2048
当我这样做时,我得到了每个系统预期峰值带宽的50%
峰值计算为频率*平均字节/时钟周期
。每个系统的平均字节/时钟周期为:
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 I上,我只观察到48字节/时钟周期(可能在一个时钟周期中有两次读取,在下一个时钟周期中有一次写入)
我打印出了b-a
和c-b
地址的差异,每个都是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;
编辑:这里是关于k=0
和k=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%
这个理论很有效。常春藤桥比我想象的要低一点,但是常春藤桥
遭受银行缓存冲突,其他人不这样做,这可能是另一个需要考虑的因素。
下面是自己测试的工作代码。在没有AVX的系统上,使用g++-O3 sum.cpp
编译,否则使用g++-O3-mavx sum.cpp
编译。尝试改变值k
//sum.cpp
#include <x86intrin.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#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, y-x %d, z-y %d\n", a, b, c, b-a, c-b);
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);
}
}
======
k=0
======
load x+(0*64+0)*4 = x+0 where x is 4k aligned 0000 000|0 0000
load y+(0*64+0)*4 = y+0 where y is 4k aligned 0000 000|0 0000
store z+(0*64+0)*4 = z+0 where z is 4k aligned 0000 000|0 0000
load x+(0*64+8)*4 = x+32 where x is 4k aligned 0000 001|0 0000
load y+(0*64+8)*4 = y+32 where y is 4k aligned 0000 001|0 0000
store z+(0*64+8)*4 = z+32 where z is 4k aligned 0000 001|0 0000
load x+(0*64+16)*4 = x+64 where x is 4k aligned 0000 010|0 0000
load y+(0*64+16)*4 = y+64 where y is 4k aligned 0000 010|0 0000
store z+(0*64+16)*4= z+64 where z is 4k aligned 0000 010|0 0000
load x+(0*64+24)*4 = x+96 where x is 4k aligned 0000 011|0 0000
load y+(0*64+24)*4 = y+96 where y is 4k aligned 0000 011|0 0000
store z+(0*64+24)*4 = z+96 where z is 4k aligned 0000 011|0 0000
load x+(0*64+32)*4 = x+128 where x is 4k aligned 0000 100|0 0000
load y+(0*64+32)*4 = y+128 where y is 4k aligned 0000 100|0 0000
store z+(0*64+32)*4= z+128 where z is 4k aligned 0000 100|0 0000
.
.
.
======
k=1
======
load x+(0*64+0)*4 = x+0 where x is 4k aligned 0000 000|0 0000
load y+(0*64+0)*4 = y+0 where y is 4k+64 aligned 0000 010|0 0000
store z+(0*64+0)*4 = z+0 where z is 4k+128 aligned 0000 100|0 0000
load x+(0*64+8)*4 = x+32 where x is 4k aligned 0000 001|0 0000
load y+(0*64+8)*4 = y+32 where y is 4k+64 aligned 0000 011|0 0000
store z+(0*64+8)*4 = z+32 where z is 4k+128 aligned 0000 101|0 0000
load x+(0*64+16)*4 = x+64 where x is 4k aligned 0000 010|0 0000
load y+(0*64+16)*4 = y+64 where y is 4k+64 aligned 0000 100|0 0000
store z+(0*64+16)*4= z+64 where z is 4k+128 aligned 0000 110|0 0000
load x+(0*64+24)*4 = x+96 where x is 4k aligned 0000 011|0 0000
load y+(0*64+24)*4 = y+96 where y is 4k+64 aligned 0000 101|0 0000
store z+(0*64+24)*4 = z+96 where z is 4k+128 aligned 0000 111|0 0000
load x+(0*64+32)*4 = x+128 where x is 4k aligned 0000 100|0 0000
load y+(0*64+32)*4 = y+128 where y is 4k+64 aligned 0000 110|0 0000
store z+(0*64+32)*4= z+128 where z is 4k+128 aligned 0001 000|0 0000
.
.
.
//sum.cpp
#包括
#包括
#包括
#包括
#定义计时器\u类型时钟\u实时
双时差(timespec开始、timespec结束)
{
时间样本温度;
如果((end.tv\u nsec-start.tv\u nsec)我认为a
和b
之间的差距并不重要。在b
和c
之间只留下一个差距之后,我在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多个时钟没有太大区别(正如@Mystical所说).TL;DR:对于k
的某些值,出现过多的4K混叠情况,这是带宽降低的主要原因。在4K混叠中,负载被不必要地暂停,从而增加了有效负载延迟,并暂停了所有后续相关指令。这反过来会导致L1带宽降低h利用率。对于k
的这些值,大多数4K混叠条件可以通过如下方式拆分循环来消除:
for(int i=0; i<n/64; i++) {
_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)));
}
for(int i=0; i<n/64; i++) {
_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)));
}
for(int i=0; i<n/64; i++) {
_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)));
}
for(int i=0; i<n/64; i++) {
_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)));
}
请注意,当k=0时,无负载似乎满足4K别名的两个条件。另一方面,当k=1时,所有负载似乎都满足这些条件。然而,手动对所有迭代和k
的所有值执行此操作非常繁琐。因此,我编写了一个程序,基本上生成内存访问的地址并计算总数对于不同的k
值,出现4K别名的加载数。我面临的一个问题是,对于任何给定的加载,我们都不知道仍在存储缓冲区中的存储数(尚未提交).因此,我设计了模拟器,以便它可以对k
的不同值使用不同的存储吞吐量,这似乎可以更好地反映真实处理器上实际发生的情况。可以找到代码
下图显示了模拟器产生的4K混叠案例的数量,与Haswell上使用LD_BLOCKS\u PARTIAL.ADDRESS\u ALIAS
测得的数量进行了比较。我已针对k
的每个值调整了模拟器中使用的存储吞吐量,以使两条曲线尽可能相似。第二个图显示了inver模拟器中使用的se存储吞吐量(总循环数除以总存储数)在Haswell上测量。请注意,k=0时的存储吞吐量并不重要,因为没有4K混叠。由于每个存储有两个负载,反向负载吞吐量是反向存储吞吐量的一半
显然,Haswell和模拟器上每个存储在存储缓冲区中的时间量是不同的,因此我需要使用不同的吞吐量使两条曲线相似。模拟器可以用来显示存储吞吐量如何影响4K别名的数量。如果存储吞吐量非常接近1c/存储,那么4K混叠情况的数量会小得多。4K混叠情况不会导致管道刷新,但可能会导致
for(int i=0; i<n/64; i++) {
_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)));
}
for(int i=0; i<n/64; i++) {
_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)));
}
for(int i=0; i<n/64; i++) {
_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)));
}
for(int i=0; i<n/64; i++) {
_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)));
}
======
k=0
======
load x+(0*64+0)*4 = x+0 where x is 4k aligned 0000 000|0 0000
load y+(0*64+0)*4 = y+0 where y is 4k aligned 0000 000|0 0000
store z+(0*64+0)*4 = z+0 where z is 4k aligned 0000 000|0 0000
load x+(0*64+8)*4 = x+32 where x is 4k aligned 0000 001|0 0000
load y+(0*64+8)*4 = y+32 where y is 4k aligned 0000 001|0 0000
store z+(0*64+8)*4 = z+32 where z is 4k aligned 0000 001|0 0000
load x+(0*64+16)*4 = x+64 where x is 4k aligned 0000 010|0 0000
load y+(0*64+16)*4 = y+64 where y is 4k aligned 0000 010|0 0000
store z+(0*64+16)*4= z+64 where z is 4k aligned 0000 010|0 0000
load x+(0*64+24)*4 = x+96 where x is 4k aligned 0000 011|0 0000
load y+(0*64+24)*4 = y+96 where y is 4k aligned 0000 011|0 0000
store z+(0*64+24)*4 = z+96 where z is 4k aligned 0000 011|0 0000
load x+(0*64+32)*4 = x+128 where x is 4k aligned 0000 100|0 0000
load y+(0*64+32)*4 = y+128 where y is 4k aligned 0000 100|0 0000
store z+(0*64+32)*4= z+128 where z is 4k aligned 0000 100|0 0000
.
.
.
======
k=1
======
load x+(0*64+0)*4 = x+0 where x is 4k aligned 0000 000|0 0000
load y+(0*64+0)*4 = y+0 where y is 4k+64 aligned 0000 010|0 0000
store z+(0*64+0)*4 = z+0 where z is 4k+128 aligned 0000 100|0 0000
load x+(0*64+8)*4 = x+32 where x is 4k aligned 0000 001|0 0000
load y+(0*64+8)*4 = y+32 where y is 4k+64 aligned 0000 011|0 0000
store z+(0*64+8)*4 = z+32 where z is 4k+128 aligned 0000 101|0 0000
load x+(0*64+16)*4 = x+64 where x is 4k aligned 0000 010|0 0000
load y+(0*64+16)*4 = y+64 where y is 4k+64 aligned 0000 100|0 0000
store z+(0*64+16)*4= z+64 where z is 4k+128 aligned 0000 110|0 0000
load x+(0*64+24)*4 = x+96 where x is 4k aligned 0000 011|0 0000
load y+(0*64+24)*4 = y+96 where y is 4k+64 aligned 0000 101|0 0000
store z+(0*64+24)*4 = z+96 where z is 4k+128 aligned 0000 111|0 0000
load x+(0*64+32)*4 = x+128 where x is 4k aligned 0000 100|0 0000
load y+(0*64+32)*4 = y+128 where y is 4k+64 aligned 0000 110|0 0000
store z+(0*64+32)*4= z+128 where z is 4k+128 aligned 0001 000|0 0000
.
.
.
%define SIZE 64*64*2
%define K_ 10
BITS 64
DEFAULT REL
GLOBAL main
EXTERN printf
EXTERN exit
section .data
align 4096
bufsrc1: times (SIZE+(64*K_)) db 1
bufsrc2: times (SIZE+(64*K_)) db 1
bufdest: times SIZE db 1
section .text
global _start
_start:
mov rax, 1000000
.outer:
mov rbp, SIZE/256
lea rsi, [bufsrc1]
lea rdi, [bufsrc2]
lea r13, [bufdest]
.loop:
vmovaps ymm1, [rsi]
vaddps ymm0, ymm1, [rdi]
add rsi, 256
add rdi, 256
add r13, 256
vmovaps[r13-256], ymm0
vmovaps ymm2, [rsi-224]
vaddps ymm0, ymm2, [rdi-224]
vmovaps [r13-224], ymm0
vmovaps ymm3, [rsi-192]
vaddps ymm0, ymm3, [rdi-192]
vmovaps [r13-192], ymm0
vmovaps ymm4, [rsi-160]
vaddps ymm0, ymm4, [rdi-160]
vmovaps [r13-160], ymm0
vmovaps ymm5, [rsi-128]
vaddps ymm0, ymm5, [rdi-128]
vmovaps [r13-128], ymm0
vmovaps ymm6, [rsi-96]
vaddps ymm0, ymm6, [rdi-96]
vmovaps [r13-96], ymm0
vmovaps ymm7, [rsi-64]
vaddps ymm0, ymm7, [rdi-64]
vmovaps [r13-64], ymm0
vmovaps ymm1, [rsi-32]
vaddps ymm0, ymm1, [rdi-32]
vmovaps [r13-32], ymm0
dec rbp
jg .loop
dec rax
jg .outer
xor edi,edi
mov eax,231
syscall