我在这个intel页面上读到了关于缓存阻塞的文章。
上面写着:
Blocking是一种众所周知的优化技术,可以帮助避免许多应用程序中的内存带宽瓶颈。阻塞背后的关键思想是通过确保数据在多个用途中保持在缓存中,来利用应用程序中固有的可用数据重用。
提供示例
for (body1 = 0; body1 < NBODIES; body1 ++) { for (body2=0; body2 < NBODIES; body2++) { OUT[body1] += compute(body1, body2); } }
在本例中,数据(body2(是从内存中流出的。假设NBODIES很大,我们就不会在缓存中重用。此应用程序受内存带宽限制。应用程序将以内存到CPU的速度运行,低于最佳速度。
优化
for (body2 = 0; body2 < NBODIES; body2 += BLOCK) { for (body1=0; body1 < NBODIES; body1 ++) { for (body22=0; body22 < BLOCK; body22 ++) { OUT[body1] += compute(body1, body2 + body22); } } }
被阻止的代码。。。是通过将body2循环拆分为在BLOCK的多个体上迭代的外循环和在块内的元素上迭代的内body22循环,并交织body1和body2环路而获得的。此代码在body1循环的多次迭代中重用一组BLOCK body2值。如果选择BLOCK,使这组值适合缓存,则内存流量会降低BLOCK的因子。
老实说,我不理解附带的解释。
我也不明白内存流量是如何下降的,以及为什么阻塞的示例会更快。
请有人帮忙解释缓存阻塞,以及它实际上是如何加速循环的?
维基百科也有一个类似的例子:
for (i=0; i<N; ++i) { ... }
可以用替换块大小为B的块
for (j=0; j<N; j+=B) { for (i=j; i<min(N, j+B); ++i) { .... } }
我不知道为什么这会更快。
为什么要利用缓存?
那篇英特尔论文很糟糕,因为它没有弄清楚索引body2
与数据在内存中的位置之间的关联,也没有弄清楚body1
与内存中数据之间的关联。其思想是OUT[body1]
将使用来自同一高速缓存块的多个元素用于body1
的几个连续值。然而,body1
的连续值的那些使用并不是一个接一个地发生的。它们是分散的,因为body2
上的整个循环都在它们之间执行。据推测,使用compute(body1, body2)
会导致根据body2
值使用各种内存。
其概念是,在body2
循环的过程中,内存中的许多不同位置将被访问,因此与OUT[body1]
相关的缓存块将从缓存中逐出,因此,当body1
增加到下一个值时,但OUT[body1]
仍在同一个缓存块中,该块不再在缓存中,必须再次加载。所以这是浪费。
将源代码更改为:
for (body2 = 0; body2 < NBODIES; body2 += BLOCK) {
for (body1=0; body1 < NBODIES; body1 ++) {
for (body22=0; body22 < BLOCK; body22 ++) {
OUT[body1] += compute(body1, body2 + body22);
}}
// Note: A closing "}" is missing in the original.
通过在改变CCD_ 14之前处理较少的CCD_。如果BLOCK
被设置为足够小,则与OUT[body1]
相关联的高速缓存块仍在高速缓存中,并且不需要从存储器重新加载。因此,在内部循环期间:
for (body1=0; body1 < NBODIES; body1 ++) {
for (body22=0; body22 < BLOCK; body22 ++) {
OUT[body1] += compute(body1, body2 + body22);
}}
用于CCD_ 17的每个高速缓存块仅从存储器加载一次并且写入存储器一次。
当然,这些内部循环在body2
上的一个新的外部循环中,因此内部循环将被执行NBODIES/BLOCK
次(本文忽略了如果NOBODIES
不能被BLOCK
完全整除时出现的块片段,我也会忽略(,因此OUT[body1]
的缓存块仍需被重新加载NBODIES/BLOCK
次,但这比重新加载它们NBODIES
次要好,其出现在原始代码中。