在确定以下两段代码的命中率和未命中率方面遇到一些麻烦。
给定信息:我们有一个1024字节的直接映射缓存,块大小为16字节。这样就可以生成64行(在这种情况下设置)。假设缓存开始为空。请考虑以下代码:
struct pos {
int x;
int y;
};
struct pos grid[16][16];
int total_x = 0; int total_y = 0;
void function1() {
int i, j;
for (i = 0; i < 16; i++) {
for (j = 0; j < 16; j++) {
total_x += grid[j][i].x;
total_y += grid[j][i].y;
}
}
}
void function2() {
int i, j;
for (i = 0; i < 16; i++) {
for (j = 0; j < 16; j++) {
total_x += grid[i][j].x;
total_y += grid[i][j].y;
}
}
}
我可以从一些基本规则(即C数组是行主要顺序)中看出,function2应该更好。但我不明白如何计算命中/未命中百分比。显然,function1()错过了50%的时间,而function2()只错过了25%的时间。
有人可以告诉我这些计算的工作原理吗?我真正看到的是,不会有超过一半的网格同时适应缓存。此外,这个概念是否易于扩展到k-way关联缓存?
感谢。
答案 0 :(得分:12)
数据如何存储在内存中
每个结构pos
的大小为8字节,因此pos[16][16]
的总大小为2048字节。阵列的顺序如下:
pos[0][0]
pos[0][1]
pos[0][2]
...... pos[0][15]
pos[1]0[]
...... pos[1][15]
....... { {1}} ...... pos[15][0]
缓存组织与数据进行比较
对于高速缓存,每个块是16字节,其大小与数组的两个元素相同。整个缓存是1024字节,是整个数组的一半。由于缓存是直接映射的,这意味着如果我们将缓存块标记为0到63,我们可以安全地假设映射应该如下所示
------------记忆----------------------------缓存
pos[15][15]
pos[0][0]
-----------&gt; pos[0][1]
block 0
pos[0][2]
-----------&gt; pos[0][3]
block 1
pos[0][4]
-----------&gt; pos[0][5]
block 2
pos[0][14]
--------&gt; pos[0][15]
.......
block 7
pos[1][0]
-----------&gt; pos[1][1]
block 8
pos[1][2]
-----------&gt; pos[1][3]
.......
block 9
pos[7][14]
--------&gt; pos[7][15]
block 63
pos[8][0]
-----------&gt; pos[8][1]
.......
block 0
pos[15][14]
-----&gt; pos[15][15]
block 63
如何操纵记忆
循环遵循逐列内循环,这意味着第一次迭代将function1
和pos[0][0]
加载到缓存pos[0][1]
,第二次迭代加载block 0
和pos[1][0]
缓存pos[1][1]
。缓存冷,因此第一列block 8
始终未成功,而x
始终被点击。在第一列访问期间,第二列数据应该全部加载到缓存中,但这是 NOT 的情况。由于y
访问权已经退出了之前的pos[8][0]
页面(它们都映射到pos[0][0]
!)。因此,未命中率为50%。
block 0
如何操纵记忆
第二个函数具有良好的 stride-1 访问模式。这意味着在访问function2
pos[0][0].x
pos[0][0].y
pos[0][1].x
时,由于冷缓存,只有第一个是未命中。以下模式都是相同的。所以未命中率只有25%。
K-way关联缓存遵循相同的分析,尽管这可能更乏味。为了充分利用缓存系统,尝试启动一个很好的访问模式,比如pos[0][1].y
,并在每次从内存加载时尽可能多地使用数据。真实世界的cpu微体系结构采用其他智能设计和算法来提高效率。最好的方法是始终测量现实世界中的时间,转储核心代码,并进行彻底的分析。
答案 1 :(得分:1)
好的,我的计算机科学讲座有点远,但我想我已经弄明白了(当你想到它时,它实际上是一个非常简单的例子)。
你的结构是8字节长(2 x 4)。由于您的缓存块是16个字节,因此内存访问grid[i][j]
将获取两个结构条目(grid[i][j]
和grid[i][j+1]
)。因此,如果循环遍历第二个索引,则每次第4次访问将导致内存读取。如果循环遍历第一个索引,则可能会丢弃已获取的第二个条目,这取决于内部循环中的提取次数与整体缓存大小的关系。
现在我们还要考虑缓存大小:你说你有64行直接映射。在函数1中,内部循环是16次读取。这意味着,第17次获取你到达网格[j] [i + 1]。这实际上应该是一个命中,因为它应该保存在自上一次内循环行走以来的缓存中。因此,每个第二内环应仅由命中组成。
好吧,如果我的推理是正确的,那么给你的答案应该是错的。两种功能都应该有25%的失误。也许有人找到了更好的答案,但如果你理解我的推理,我就会向TA询问。
编辑:再考虑一下,我们应该首先定义实际符合未命中/命中的内容。当你看到
total_x += grid[j][i].x;
total_y += grid[j][i].y;
这些被定义为两个内存访问还是一个?具有优化设置的合适编译器应将其优化为
pos temp = grid[j][i];
total_x += temp.x;
total_y += temp.y;
可以算作一次内存访问。因此,我提出对所有CS问题的普遍回答:“这取决于。”