计算机系统结构总结_Cache Optimization
Textbook:
《计算机组成与设计——硬件/软件接口》 HI
《计算机体系结构——量化研究方法》 QR
Ch4. Cache Optimization
本章要讨论的问题就是 How to Improve Cache Performance?
前面讲过 Average memory access time = HitTime + (MissRate * MissPenalty)
那么我们的方向就是Reduce MissRate / HitTime / MissPenalty
1. 6 Basic Cache Optimization(PPT P3)
• Reducing hit time
1. Giving Reads Priority over Writes
• E.g., Read complete before earlier writes in write buffer ??
2. Avoiding Address Translation during Cache Indexing
Cache中使用虚拟地址,这样就可以同时Access TLB和Cache / Access Cache firstly
• Reducing Miss Penalty
3. Multilevel Caches
AMAT = Hit TimeL1 + Miss RateL1 x Miss PenaltyL1
Miss PenaltyL1 = Hit TimeL2 + Miss RateL2 x Miss PenaltyL2
原来Miss PenaltyL1要访问内存,很慢。现在多了L2
• Reducing Miss Rate
4. Larger Block size (Compulsory misses)
...
5. Larger Cache size (Capacity misses)
...
6. Higher Associativity (Conflict misses)
...
2. 11 Advanced Cache Optimizations (PPT P12)
• Reducing hit time
1. Small and simple caches(QR P59)
如果仅考虑Cache Hit Time,那么结构越简单、容量越小、组相连路数越少的缓存肯定是越快的。
所以出于速度考虑,CPU的L1缓存都是很小的。比如从Pentium MMX到Pentium 4,L1缓存的容量都没有增长。
不过太少了肯定也是不行的。。。所以这也是一个Trade off
2. Way prediction
直接相连的Hit Time是很快的,但conflict miss多。组相连可以减少conflict miss,但结构复杂功耗也高一些,hit time也多一点。那么有什么方法能两者兼得呢?
因为组相连缓存中,每一个组里面的N个路(block)是全相连的。也就是相当于读的时候,每次映射好一个set之后,要遍历一遍N个block,当N越大的时候费的时间就越多。有一种黑科技方法叫做路预测(way prediction),它的思想就是在缓存的每个块中添加预测位,来预测在下一次缓存访问时,要访问该组里的哪个块。当下一次访问时,如果预测准了就节省了遍历的时间(相当于直接相连的速度了);如果不准就再遍历呗。。。
好在目前这个accuracy还是很高的,大概80+%了。
不过有个缺点就是Hit Time不再是确定的几个cycle了(因为没命中的时候要花的cycle多嘛),不便于后面进行优化(参考CPU pipeline)。
3. Trace caches
这个只针对instruction cache。在读取指令缓存时,要不断jump来读取不同的指令(也就是比较random的Access pattern),这样就不如sequential的access快了。
在牙膏厂的Pentium 4中,使用了trace caches的黑科技。它会尝试找出相邻被访问的指令(比如A jump to B),然后把这些block放到邻近的位置,这样就可以access instruction cache sequentially。
但因为现在code reuse rate不高了(程序太多了,很多程序可能一段时间内只执行一次),再加上这个黑科技implement比较复杂,后来就放弃了。
• Increasing cache bandwidth
4. Pipelined caches
在本科的计组课上我们学过pipeline的思想。在cache访问中也可以使用pipeline技术。
但pipeline是有可能提高overall access latency的(比如中间有流水线气泡),而latency有时候比bandwidth更重要。所以很多high-level cache是不用pipeline的
5. cache with Multiple Banks
对于Lower Level Cache(比如L2),它的read latency还是有点大的。假设我们有很多的cache access需要访问不同的数据,能不能让它们并行的access呢?
可以把L2 Cache分成多个Bank(也就是多个小分区),把数据放在不同Bank上。这样就可以并行访问这几个Bank了。
那么如何为数据选一个合适的Bank来存呢?一个简单的思路就是sequential interleaving:Spread block addresses sequentially across banks. E,g, if there 4 banks, Bank 0 has all blocks whose address modulo 4 is 0; bank 1 has all blocks whose address modulo 4 is 1; ...... 因为数据有locality嘛,把相邻的块存到不同bank,就可以尽量并行的访问locality的块了
6. Nonblocking caches
假设要执行下面一段程序:
1 Reg1:=LoadMem(A); 2 Reg2:=LoadMem(B); 3 Reg3:=Reg1 + Reg2;
当执行第一行时,cpu发现地址A不在cache中,就需要去内存读。但读内存的时间是很长的,此时CPU也不会闲着,就去执行了第二行。然后发现B也不在cache中。那么此时cache会怎么做呢?
- (a). cache阻塞,等着先把A读进来,然后再去读B。这种叫做Blocking Cache
- (b). cache同时去内存读B,最终B和A一起进入Cache。这种叫做Non-Blocking Cache
可以看出Non-Blocking Cache应该是比较高效的一种方法。在这种情况下,两条语句的总执行时间就只有一个miss penalty了:
(图中只是大概的描述,不是精确的时间计算。。。如果用了上面介绍的multiple bank cache,那么hit时间可能也只需要一次了,很棒棒吧!)
• Reducing Miss Penalty
7. Early Restart and Critical word first
相对一个Word来说,cache block size一般是比较大的。有时候cpu可能只需要一个block中的某一个word,那么如果cpu还要等整个block传输完才能读这个word就有点慢了。因此我们就有了两种加速的策略:
- Critical Word First:首先从存储器中读想要的word,在它到达cache后就立即发给CPU。然后在载入其他目前不急需的word的同时,CPU就可以继续运行了
- Early Restart:或者就按正常顺序载入一整个block。当所需的word到达cache后就立即发给CPU。然后在载入其他目前不急需的word的同时,CPU就可以继续运行了
大概就是这个意思:
根据locality的原理,一般来说CPU接下来要访问的也就是这个block中的剩余内容。所以没毛病!
8. Merging write buffers
?????(QR P65)
• Reducing Miss Rate
9. Compiler optimizations
这是最喜闻乐见的一种方法了hhhh
这里的reducing miss rate又可以分为Instruction miss和data miss两类:
Instruction Miss:
• Reorder procedures in memory so as to reduce conflict misses
• Profiling to look at conflicts(using tools they developed) (之前面试还被问到过Linux profiling了......)
Data Miss:这个是比较重要的一种方式了。网上很多大神所说的黑科技优化C代码的原理就是这个。
- 1. Merging Arrays: improve spatial locality by single array of compound elements vs. 2 arrays
假设有下面两个定义(他们的功能都是一样的,只是写法不同):
/* Before: 2 sequential arrays */ int val[SIZE]; int key[SIZE]; /* After: 1 array of stuctures */ struct merge { int key; int val; }; struct merge merged_array[SIZE];
我们可以比较一下对于这两种定义方式,它们在内存中的组织方式:
好的现在我们要对index k,分别访问key[k]和val[k]。
/* Before: Miss Rate = 100% */ int k=rand(k); int _key=key[k]; int _val=val[k]; /* After: Miss Rate = 50% */ int k=rand(k); int _key=dat[k].key; int _val=dat[k].val;
可以看出第二种方式充分利用了spatial locality。对于同一个index k,读取key_k的同时,val_k也被读进cache啦,这样就节省了一次访问内存的时间。
上面这个还可以引申出另一个话题,叫做结构体对齐。
- 2. Loop Interchange: change nesting of loops to access data in order stored in memory
还是下面两种程序,它们只是循环次序改变了:
int x[][]; //very large
//Assume a cacheline could contain 2 integers.
/* Before */ for (j = 0; j < 100; j = j+1) for (i = 0; i < 5000; i = i+1) x[i][j] = 2 * x[i][j];
/* After */ for (i = 0; i < 5000; i = i+1) for (j = 0; j < 100; j = j+1) x[i][j] = 2 * x[i][j];
我们知道在C语言中,二维数组在内存中的存储方式是Row Major Order的,也就是这样:
那么对于第一种写法,访问顺序是x[0][0], x[1][0], x[2][0], ......。Miss Rate达到了100%
第二种写法,访问顺序是x[0][0], x[0][1], x[0][2], x[0][3], ......。读x[0][0]的时候可以把x[0][1]也读进来,读x[0][2]的时候可以把x[0][3]也读进来,以此类推。这样Miss Rate就只有50%啦
- 3. Loop Fusion: Combine 2 independent loops that have same looping and some variables overlap
来看个例子:
1 /* Before */ 2 for (i = 0; i < N; i = i+1) 3 for (j = 0; j < N; j = j+1) 4 a[i][j] = 1/b[i][j] * c[i][j]; 5 for (i = 0; i < N; i = i+1) 6 for (j = 0; j < N; j = j+1) 7 d[i][j] = a[i][j] + c[i][j]; 8 9 10 /* After */ 11 for (i = 0; i < N; i = i+1) 12 for (j = 0; j < N; j = j+1){ 13 a[i][j] = 1/b[i][j] * c[i][j]; 14 d[i][j] = a[i][j] + c[i][j]; 15 }
在第二种写法中,line 13已经把a[i][j]和c[i][j]读进cache了,line14就可以接着用了。加起来比第一种要省很多cache miss。
不过第一种写法本身时间复杂度也高啊。。。这样写代码会被人打的。。。
emmm上面这个例子比较弱智。。。下面再来看一个经典的Matrix Multiplication的例子:
假设我们要计算一个大矩阵的乘法,然后cache block是4个integer的大小。
矩阵乘法是三重循环,O(N^3)的。我们来分析不同的循环顺序下,最内层循环的cache miss情况(因为cache很小,只会在最内层循环起作用,外面的肯定都要有miss的):
- 4. Blocking: Improve temporal locality by accessing “blocks” of data repeatedly vs. going down whole columns or rows
从上面的例子中可以看到,当每次access的是同一column中的不同row(a[1][3], a[2][3], a[3][3], a[4][3], ......),而不是同一row的不同colum时,miss rate是很可怕的。那么怎么避免这一现象呢?
一种思路是我们把整个大矩阵分解成若干个小矩阵(以所需的数据能被cache全部装下为标准),然后每次都把这个小块内要计算的任务全部完成,这样就不用access whole column了。
/* Before */ for (i = 0; i < N; i = i+1) for (j = 0; j < N; j = j+1){ r = 0; for (k = 0; k < N; k = k+1) r = r + y[i][k]*z[k][j]; x[i][j] = r; } /* After */ for (jj = 0; jj < N; jj = jj+B) for (kk = 0; kk < N; kk = kk+B) for (i = 0; i < N; i = i+1) for (j = jj; j < min(jj+B-1,N); j = j+1){ r = 0; for (k = kk; k < min(kk+B-1,N); k = k+1){ r = r + y[i][k]*z[k][j]; } x[i][j] = x[i][j] + r; }
其中B叫做Blocking Factor。(QR P67)
• Capacity Misses from 2N3 + N2 to 2N3/B +N2
• Conflict Misses Too?(没讲)
Blocking Transformation
其实前面提到的这些access pattern现在已经可以被compiler自动优化了,所以也算是上古时代的黑科技了......
• Reducing miss penalty or miss rate via parallelism
10. Hardware prefetching
假设cache block只能装下一个int,然后我们有如下指令:
int a[]; load a[0]; load a[1]; load a[2]; load a[3]; load a[4]; load a[5];
那么与其每次都cache miss重新载入,不如在第一次cache miss(load a[0])时,让cache预测到接下来会用到a[1], a[2], a[3], ......,然后提前载入到next level cache里备用。这就是硬件的prefetching。
对于Instruction Prefetching,CPU fetches 2 blocks on a miss: the requested block and the next consecutive block.(Requested block is placed in instruction cache when it returns, and prefetched block is placed into instruction stream buffer)
对于Data Prefetching,Pentium 4 can prefetch data into L2 cache from up to 8 streams from 8 different 4 KB pages. Prefetching invoked if 2 successive L2 cache misses to a page, or if distance between those cache blocks is < 256 bytes.
但hardware prefetching只对比较predictable的access pattern(特别是instruction prefetching)起作用。如果是访问一个动态链表那就不管用了......
11. Compiler prefetching
????(QR P69)
最后是对这些cache optimization的一个总结(QR P72):
...
posted on 2019-10-04 15:56 Pentium.Labs 阅读(4135) 评论(0) 编辑 收藏 举报