最近,我对面向数据的设计产生了兴趣。到目前为止,我已经阅读了一些有关该主题的文章和出版物。我了解缓存是如何工作的以及它是如何构建的(一般地,抽象地)L1,L2,L3,什么是缓存行,什么是N路关联,为什么缓存如此高效等等。不幸的是,我在理论上理解这一切,但看例子我不太理解它们。另外,我无法将其合并为一个,因此我的问题。
我在互联网上发现了一个示例(http://igoro.com/archive/gallery-of-processor-cache-effects/)(示例3 -> 下面的代码)。
我的问题:
我不明白为什么性能会随着数组大小的增加而下降(参见链接中的图片)。
毕竟,缓存行是 64 字节(我在某处读到这是这个大小),所以它应该始终适合 L1。我不明白数据大小和给定缓存级别之间的关系,因为缓存行是 64 字节 + 预取器 256 字节(我在某处读到这是大小) 另外,所以一切都适合 L1。 (如果缓存行繁忙,那么我们可以将其删除并添加新的所需缓存行。)。
int steps = 64 * 1024 * 1024; // Arbitrary number of steps int lengthMod = arr.Length - 1; for (int i = 0; i < steps; i++) { arr[(i * 16) & lengthMod]++; // (x & lengthMod) is equal to (x % arr.Length) }
您可以看到 32kB 和 4MB 之后明显下降——L1 和 L2 的大小 缓存在我的机器上。
另外:我不明白如何将数据加载到缓存中。我们假设所需的数据不在任何级别的缓存中,并且整个缓存为空。
数据是否会首先加载到 L3,然后加载到 L2 和 L1(如果适合)?还是只达到L1?
我想答案并不明确,取决于CPU和缓存设计,但如果有人能以某种通用的方式描述它,我将非常感激。
您的第一个问题:
为什么性能会随着数组大小的增加而下降?
作者声称他的缓存大小为“32kB L1 数据缓存、32kB L1 指令缓存和 4MB L2 数据缓存”。所以即使像你说的那样单个缓存行是 64 字节,每个缓存都有多个缓存行。
话虽如此,数组没有理由“应该始终适合 L1”。作者每次都使用不同大小的数组重新运行此代码片段(代码中未显示数组大小声明),大小在 1KB 到 1GB 之间。
理论上*,根据作者的缓存大小,小于 32KB 的数组将适合他的 L1 和 L2 缓存,并且当第一次需要将整个数组加载到缓存中时,他只会出现冷未命中。
一旦数组大于 32KB,现在他将拥有原始的冷未命中以及由于某些数据必须被逐出并重新加载到 L1 缓存中而导致的额外 L1 未命中。
任何大于 4MB 的数据,现在他都会有额外的 L2 未命中。
对于您的其他问题:
数据是否会首先加载到 L3,然后加载到 L2 和 L1(如果适合)?还是只达到L1?
这取决于微架构。请参阅此处。
* 在该阵列程序进行时,其他进程也会竞争缓存行,从而增加实际的缓存未命中率。