我正在阅读Bryant&O'Hallaron的《计算机系统》一书,其中有一项练习似乎解决方案不正确。所以我想确定
给定
struct point {
int x;
int y; };
struct array[32][32];
for(i = 31; i >= 0; i--) {
for(j = 31; j >= 0; j--) {
sum_x += array[j][i].x;
sum_y += array[j][i].y; }}
sizeof(int) = 4;
我们有4096字节的缓存,块(行)大小为32字节。 要求命中率。
我的推理是,我们有4096/32 = 128
个块,每个块可以存储4点(2*4*4 = 32
),因此缓存可以存储数组的1/2,即512点(总计32 * 32) = 1024)。由于代码按列主要顺序访问数组,因此错过了对每个点的访问。因此,array[j][i].x
总是被错过,而array[j][i].y
被命中。最后,未命中率=命中率= 1/2 。
问题::该解决方案表示命中率为3/4,因为缓存可以存储整个阵列。
但是根据我的推理,缓存只能存储一半的点数
我错过了什么吗?
答案 0 :(得分:1)
数组的前四行占据了缓存的一部分:
|*ooooooooooooooooooooooooooooooo|
|*ooooooooooooooooooooooooooooooo|
|*ooooooooooooooooooooooooooooooo|
|*ooooooooooooooooooooooooooooooo|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|...
以上是阵列的示意图,因为应用数学家会将该阵列写在纸上。每个元素由一个(x,y)对,一个 point 。
图中的标记为o
的四行包含128个点,足以填充1024个字节,仅占缓存的四分之一,但请参见:在代码中,变量i
是
因此,让我们再次看一下该图。如图所示,嵌套循环如何遍历数组?
答案:显然,您的循环如图所示在顶部行向右移动,j
(列)为次要循环计数器。但是,如您所见,该数组按列存储。因此,在加载元素[j][i] == [0][0]
时,将加载整个缓存行。高速缓存行包括什么?这是图中标记为*
的四个元素。
因此,虽然您的内部循环遍历如图所示的数组顶部行,但是每次缓存都会丢失,每次都获取四个元素。然后接下来的三行都是热门。
这不容易想到。这是一个很好的问题,也不希望您立即理解我的答案,但是如果您仔细地考虑了我所解释的负载顺序,那么(经过一番思考之后)应该就有意义了。
在给定的循环嵌套下,命中率确实是3/4。
进一步的讨论
在评论中,您提出了一个很好的后续问题:
您可以写一个可以命中的元素(例如
array[3][14].x
)吗?
我可以。 array[j][i] == array[10][5]
会命中。 (.x
和.y
都会命中。)
我会解释。 array[j][i] == array[10][4]
会错过,而array[10][5]
,array[10][6]
和array[10][7]
最终会命中。为什么最终?这很重要。尽管我命名的所有四个元素都是由缓存硬件立即加载的,但是当array[10][5]
为array[10][4]
时,代码不能访问(即由CPU)访问array[10][4]
访问。相反,在访问array[11][4]
之后,程序和CPU接下来将访问array[10][5]
。
程序和CPU只能在以后访问array[column][row] == array[j][i]
。
的确,如果您考虑一下,这是有道理的,不是吗,因为这是缓存的一部分:它们现在作为缓存行的一部分安静地加载其他数据,以便CPU可以以后在需要时快速访问其他数据。
附录:FORTRAN / BLAS / LAPACK矩阵订单
在数值计算中,标准是按列而不是按行存储矩阵。这称为主要列存储。不幸的是,与早期的Fortran编程语言不同,C编程语言最初不是为数字计算而设计的,因此,在C语言中,要按列存储数组,必须写[j][i]
,这当然会颠倒应用程序的方式。数学家用他或她的铅笔会写。
这是C编程语言的产物。该工件没有数学意义,但是在使用C进行编程时,必须记住键入(i, j)
。 [如果您使用现在已经过时的Fortran编程语言进行编程,则将键入{{1}},但这不是Fortran。]
列主要存储为标准的原因与在数学/铅笔术语中对列向量左操作矩阵[A]时CPU执行标量,浮点乘法和加法的顺序有关X。 LAPACK等使用的标准基本线性代数子例程(BLAS)库以这种方式工作。您和我也应该以这种方式工作,不仅是因为我们可能需要与BLAS和/或LAPACK交互,而且从数字上讲,它更平滑。
答案 1 :(得分:1)
如果您正确转录了程序,那么您是正确的,那么3/4的答案是错误的。
如果最里面的sum += ...
语句中的索引的排列方式使最右边的索引变化最快,即:
sum_x += array[i][j].x;
sum_y += array[i][j].y;
在那种情况下,循环的第1,第5,第9 ...迭代将丢失,但是每个未命中加载到缓存中的行将导致接下来的三个迭代命中。
但是,按照编写的程序,每次迭代都会丢失。从内存加载的每个高速缓存行仅提供一个点的数据,然后在访问该行中其他三个点中的任何一个点之前,总是替换该行。
作为一个示例(为简单起见,假设第一个成员array[0][0]
的地址与缓存的开头对齐),在循环的第一遍中对array[31][31]
的引用是未命中的这会导致加载缓存的第127行。第127行现在包含[31][28]
,[31][29]
,[31][30]
和[31][31]
的数据。但是,对array[15][31]
的提取导致在引用array[31][30]
之前第127行被覆盖,因此,当[31][30]
的转弯最终到达时,也将丢失。然后[15][30]
的未命中替换了引用[31][29]
之前的行。
IMO,您的1/2命中率过高,因为它将对.y
坐标的访问视为命中。但是,这不是原始3/4答案的作用。如果将.y
坐标的获取计为一次命中,则原始答案将为7/8。取而代之的是,它会将每个完整点或每个循环迭代视为一次命中或未命中。通过该度量,问题中编写的程序的命中率是一个不错的回合0。