这种情况下,数据需要重用reused,但是数据集比cache大,造成capacity和conflict miss。通过分裂数据集,一次处理一个数据子集,可以避免这种miss,这种方法叫做blocking或tiling. 下面以例子说明原因和处理方法。点积函数,调用4次,一个参考矢量,四个不同的输入矢量。
short in1[N];
short in2[N];
short in3[N];
short in4[N];
short w [N];
r1 = dotprod(in1, w, N);
r2 = dotprod(in2, w, N);
r3 = dotprod(in3, w, N);
r4 = dotprod(in4, w, N);
假定每个数组都是L1D cache容量的两倍,对w来说,除了第一次调用需要compulsory miss外,之后应该保存在cache里面重用是最合理的,但分析可知,当处理到N/4的输入数据时,最先进入cache的w就开始被驱逐了,这样w将会被反复多次读入cache,非常浪费。可以考虑加个循环,每次只处理N/4的数据,保证w在读进cache后,直到用完才驱逐,修改如下:
for (i=0; i<4; i++) {
o = i * N/4;
dotprod(in1+o, w+o, N/4);
dotprod(in2+o, w+o, N/4);
dotprod(in3+o, w+o, N/4);
dotprod(in4+o, w+o, N/4); }
可以利用miss pipelining进一步减少read miss stalls.在每次循环开始用touch循环预先在cache分配w[],这样在每次调用点积函数前,需要的输入数组都准备好了:
for (i=0; i<4; i++) {
o = i * N/4;
touch(w+o, N/4 * sizeof(short));
touch(in1+o, N/4 * sizeof(short));
dotprod(in1+o, w+o, N/4);
touch(w+o, N/4 * sizeof(short)); //++每次touch in[]前都要touch w[]是为了保证w[]为MRU,
touch(in2+o, N/4 * sizeof(short)); //++以防访问顺序发生改变导致w[]被驱逐掉。
dotprod(in2+o, w+o, N/4);
touch(w+o, N/4 * sizeof(short));
touch(in3+o, N/4 * sizeof(short));
dotprod(in3+o, w+o, N/4);
touch(w+o, N/4 * sizeof(short));
touch(in4+o, N/4 * sizeof(short));
dotprod(in4+o, w+o, N/4); }
另外,本例中为避免bank conflict,数组w[]和in[]应该对齐到不同的memory banks:
#pragma DATA_SECTION(in1, ".mydata")
#pragma DATA_SECTION(in2, ".mydata")
#pragma DATA_SECTION(in3, ".mydata")
#pragma DATA_SECTION(in4, ".mydata")
#pragma DATA_SECTION(w , ".mydata")
#pragma DATA_ALIGN(w, CACHE_L1D_LINESIZE) //++意味着已#pragma DATA_MEM_BANK(w, 0)
short w [N];
#pragma DATA_MEM_BANK(in1, 2) //++avoid bank conflicts
short in1[N];
short in2[N];
short in3[N];
short in4[N];
这个例子in1~in4的容量为L1D cache的两倍,假如为32k,这样,假定已经从头对齐,按照上面变量的定义顺序,则in1的0~8k将映射到 cache的way0-set0开始,9~16k映射到 cache的way1-set0开始,17~24k映射到 cache的way0-set0开始,25~32k映射到 cache的way1-set0开始,in2的0~8k映射到 cache的way0-set0开始......这样布置后,w刚好从way0-set0开始映射。由此,dotprod(in1,w,N)开始后,in1与w分别进入set0-way0/way1,......到N/4时,超过cache的8k/way容量,9~16k的In1开始进入set0-way0,这是合理可接受的,因为in1的前8k line data已经不再需要了;但同时w的9~16k也开始进入set0-way1,将其前8k的line data替换掉了,这就不合理了,因为后面计算in2的时候还需要用到w的0~8k。这样分析后,可见使用一个简单循环就避免了这个问题。
这个例子的启示是:数据排放不变(有时需要定义的数组太大,必须连续的空间,排放时不方便灵活处理),通过改变程序,从而改变使用数据的顺序,一样可以达到一line data进入cache后,直到用完才释放的cache使用终极目的。
>避免write buffer相关的stalls
WB只有4个入口,且深度有限,如果WB满,而又出现写miss,则CPU就会stall直到有WB有空间为止。同时,read miss会使得write buffer完全停止,因此保证正确的read-after-write顺序非常重要(read miss需要访问的数据很可能仍然在WB中)。通过在L1D cache中分配输出buffer(事先将输出buffer cache进入L1D),可以完全避免WB相关的stalls,这样write操作会在输出buffer中hit,而非由WB写出。事实上,输出buffer是在循环执行过程中逐渐进入L1D的,在此过程中还是会存在read miss的。
void vecaddc(const short *restrict x, short c, short *restrict r, int nx)
{ int i;
for (i = 0 ; i < nx; i++)
r = x + c;
}
{
short in[4][N];
short out [N];
short ref [N];
short c, r;
for (i=0; i<4; i++)
{
vecaddc(in, c,out,N);
r = dotprod(out, ref, N);
}
}
|