【问题标题】:Hit / Miss rate counting by array caching通过数组缓存计算命中/未命中率
【发布时间】:2019-07-04 01:27:16
【问题描述】:

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

但根据我的推理,缓存只能存储半个点

我错过了什么吗?

【问题讨论】:

  • sizeof(int) = 4;
  • 你说得对,存储顺序是这样的。因此,通过读取point[j][i].xpoint[j][i].y 由于空间局部性而存储在缓存中(在我们的例子中,点 = 数组)
  • 例如通过访问array[0][0].x,我们将array[0][0].yarray[0][1].xarray[0][1].y、...、array[0][3].xarray[0][3].y 加载到第一个块中
  • @thb 我在等。顺便说一句,在我看来,很明显缓存不能存储整个数组,而解决方案说它可以

标签: c caching cpu-architecture


【解决方案1】:

如果您正确转录了程序,那么您是正确的,3/4 答案是错误的。

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] 的获取导致第 127 行在 array[31][30] 被引用之前被覆盖,因此当最终轮到 [31][30] 时,这也是一个失误。然后[15][30] 处的未命中替换了引用[31][29] 之前的行。

IMO 您的 1/2 命中率过于慷慨,因为它将对 .y 坐标的访问视为一次命中。然而,这不是原来的 3/4 答案所做的。如果.y 坐标的获取被计为命中,那么原始答案将是 7/8。相反,它将每个完整点或每个循环迭代都计为命中或未命中。通过这个衡量,您问题中所写程序的命中率是一个不错的第 0 轮。

【讨论】:

  • 你是对的,如果我们计算错过率 w.r.t.数组的元素,那么未命中率将为 0。但我认为我们应该计算它 w.r.t。读操作。顺便谢谢你的回答
  • 顺便说一句,你用的是“全球版”的书吗?作者在其网站csapp.cs.cmu.edu/3e/errata.html 上有一份声明,称该版本中的练习已被出版商替换,并且已知存在错误。
  • 源代码在 C 中,因此无法确定实际发生了什么(例如,编译器的优化器可能会转换循环,以便以良好的顺序模式访问数组)。
【解决方案2】:

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

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

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

图中标记为o 的四行包含128 个点,足以填充1024 个字节,这只是缓存的四分之一,但请参阅:在您的代码中,变量@987654323 @是

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

那么,让我们再看一下图表。如图所示,您的嵌套循环如何遍历数组?

答案:显然,如图所示,您的循环向右跨过顶行,j(列)作为次要循环计数器。但是,正如您所观察到的,数组是按列存储的。因此,当加载元素[j][i] == [0][0] 时,会加载整个缓存行。那条缓存线是由什么组成的?就是图中标记为* 的四个元素。

因此,当您的内部循环如图所示遍历数组的顶行时,缓存每次都未命中,每次都获取四个元素。然后对于接下来的三行,都是命中。

这并不容易考虑。这是一个很好的问题,我也不希望你立即掌握我的答案,但如果你仔细考虑我解释的加载顺序,它应该(经过一番思考)开始有意义。

使用给定的循环嵌套,命中率确实是 3/4。

进一步讨论

在 cmets 中,您提出了一个很好的后续问题:

你能写一个会命中的元素(例如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][4] 时,array[10][5] 不会被您的代码访问(即 CPU)。相反,在访问array[10][4] 之后,array[11][4] 下一次被程序和 CPU 访问

程序和 CPU 只能稍后访问array[10][5]

而且,事实上,如果你仔细想想,这是有道理的,不是吗,因为这是缓存的一部分:它们现在加载额外的数据,作为缓存行的一部分悄悄地,以便 CPU 可以如果需要,稍后可以快速访问其他数据。

附录:FORTRAN/BLAS/LAPACK 矩阵排序

数值计算的标准是按列而不是按行存储矩阵。这称为 column-major 存储。不幸的是,与早期的 Fortran 编程语言不同,C 编程语言最初并不是为数值计算而设计的,因此,在 C 中,要按列存储数组,必须写为 array[column][row] == array[j][i]——这当然与应用数学家的方式相反他或她的铅笔会写出来。

这是 C 编程语言的产物。该工件没有数学意义,但在使用 C 编程时,您必须记住键入 [j][i]。 [如果您使用现在大多已过时的 Fortran 编程语言进行编程,您会输入 (i, j),但这不是 Fortran。]

列优先存储成为标准存储的原因与 CPU 执行标量、浮点乘法和加法的顺序有关,在数学/铅笔术语中,矩阵 [A] 对列向量进行左操作X。 LAPACK 和其他人使用的标准基本线性代数子程序 (BLAS) 库以这种方式工作。你和我也应该以这种方式工作,不仅因为我们可能需要与 BLAS 和/或 LAPACK 交互,还因为从数字上讲,它更流畅。

【讨论】:

  • 我还在看,但同时缓存可以存储while数组的语句呢?
  • 我不知道为什么这么说。我同意:这对我来说似乎是印刷错误。没有教科书是完美的,但是对于 4 字节整数,每个 点 8 个字节, 在我看来,缓存只能存储数组的一半。不过有趣的是,考虑到循环嵌套的顺序,这个缓存可以稍微小一些而不影响命中率。在您提交作业问题并收到已标记的论文后,如果您仍然确定您是对的,您可以通过出版商联系该书的作者以更正此错误打印。
  • 你能写一个会命中的元素(例如array[3][14].x)吗?我可能会理解或提供相反的观点
  • 特别是如果你一次简洁地在不同的页面上提交了几个更正,你可能会在本书下一版的序言中获得荣誉,这在专业上值得一提。
  • 所以,由于array[i][j].x 总是被命中,我假设你的意思是array[10][5].y 会被命中。但在我看来,元素(甚至是 while 行)将在代码访问它之前被驱逐。我会尝试找到我认为会驱逐它的确切元素
猜你喜欢
  • 1970-01-01
  • 2018-07-22
  • 1970-01-01
  • 1970-01-01
  • 2013-04-30
  • 2014-06-26
  • 2014-07-29
  • 2012-05-23
  • 2019-10-05
相关资源
最近更新 更多