命中率/未命中率通过数组缓存计数

时间:2019-02-10 10:37:50

标签: c caching cpu-architecture

我正在阅读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,因为缓存可以存储整个阵列。

但是根据我的推理,缓存只能存储一半的点数

我错过了什么吗?

2 个答案:

答案 0 :(得分:1)

数组的前四行占据了缓存的一部分:

|*ooooooooooooooooooooooooooooooo|
|*ooooooooooooooooooooooooooooooo|
|*ooooooooooooooooooooooooooooooo|
|*ooooooooooooooooooooooooooooooo|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|
|...

以上是阵列的示意图,因为应用数学家会将该阵列写在纸上。每个元素由一个(x,y)对,一个 point

图中的标记为o的四行包含128个,足以填充1024个字节,仅占缓存的四分之一,但请参见:在代码中,变量i

  • 主要循环计数器以及
  • 数组的 row 索引(写在纸上)。

因此,让我们再次看一下该图。如图所示,嵌套循环如何遍历数组?

答案:显然,您的循环如图所示在顶部行向右移动,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 += ...语句中的索引的排列方式使最右边的索引变化最快,即:

,则3/4答案是正确的。
    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。