让我们考虑两个几乎完全相同的代码:
首先
for (int k=0;k<1000;k++)
{
for (int i=0;i<600;i++)
{
for (int j=0;j<600;j++)
{
tab[i][j] = i *j;
}
}
}
第二
for (int k=0;k<1000;k++)
{
for (int i=0;i<600;i++)
{
for (int j=0;j<600;j++)
{
tab[j][i] = i *j;
}
}
}
在第二个而不是tab [i] [j]中,我们有tab [j] [i]。
第一个代码要快得多。
问题
为什么第一个代码要快得多?
我的直觉
是因为当程序试图访问一个单元时,首先包含该单元的整个块移动到缓存,然后通过缓存访问它。由于存储器中的数组由连续的单元表示,因此在第一种情况下,在第一种情况下,对存储器的访问比在第二种情况下少得多。
答案 0 :(得分:2)
这是因为缓存局部性。处理器高速缓存行可以同时保存多个数组元素,但仅限于邻接地址。
在第一种情况下,您有更多缓存命中 - 当您遍历第二个数组索引时,您访问相邻元素。您访问某个元素,处理器将其及其邻居加载到缓存行中,下一个相邻的访问会产生缓存命中 - 您不再需要访问内存来处理它们。
在第二种情况下,当你迭代第一个索引加载一些元素时,一行缓存被填充,但下一次访问是不在同一行的元素。这使得处理器将另一条线加载到缓存中。如果缓存不能同时保存所有行,则必须丢弃先前加载的行并稍后重新加载它们。这大大增加了内存访问次数,从而增加了执行时间
答案 1 :(得分:2)
除了其他响应中正确识别的问题之外,还有一个次要问题,即大多数现代CPU都具有自动预取功能。当从顺序地址加载一定数量的高速缓存行时,则启动自动预取,并且推测性地加载另外的高速缓存行。如果结果消除了DRAM延迟的影响,这可能是一次巨大的性能提升。如果你不按顺序访问内存,那么你就不会获得这个好处,如果预取加载了以后不需要的缓存行,它甚至可能适得其反。
答案 2 :(得分:1)
是的,你的理论是正确的。
当访问整个阵列中的单个元素时,内存必须切换进出缓存,因为整个数组太大而无法放入缓存中。
当您按顺序访问元素时,每个内存块只需进入和退出缓存一次。另外,由于您只使用缓存中的最后一个块,因此可以在最方便的时候将先前的块写回内存。