ini
int array[][] = {
{11, 12, 13, 14, 15},
{21, 22, 23, 24, 25},
{31, 32, 33, 34, 35}
};

局部性原理
这里只讨论空间局部性
- CPU 读取内存 (速度慢) 数据后,会将其放入高速缓存 (速度快) 当中,如果后来的计算再用到此数据,在缓存中能读到的话,就不必读内存了
- 缓存的最小存储单位是缓存行 (cacheline),一般是 64 bytes,一次读取的数据少了不划算啊,因此最少读 64 bytes 填满一个缓存行,因此读入某个数据时也会读取其邻近的数据,这就是所谓空间局部性
下面代码中StopWatch是spring-core-5.2.3.RELEASE.jar 导入即可使用
ini
public class TestCacheLine
{
public static void ij(int a[][], int rows, int columns)
{
long sum = 0L;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < columns; j++)
{
sum += a[i][j];
}
}
System.out.println(sum);
}
public static void ji(int a[][], int rows, int columns)
{
long sum = 0L;
for (int i = 0; i < columns; i++)
{
for (int j = 0; j < rows; j++)
{
sum += a[j][i];
}
}
System.out.println(sum);
}
/*
CPU 缓存 内存
皮秒 毫秒
64字节
缓存行 cache line
空间局部性
*/
public static void main(String[] args)
{
int rows = 1_000_000;
int columns = 14;
int a[][] = new int[rows][columns];
StopWatch stopWatch = new StopWatch();
stopWatch.start("ij");
ij(a, rows, columns);
stopWatch.stop();
stopWatch.start("ji");
ji(a, rows, columns);
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
}
打印结果:
markdown
0
0
StopWatch '': running time = 192431100 ns
---------------------------------------------
ns % Task name
---------------------------------------------
041449500 022% ij
150981600 078% ji
为什么两者之间有这么大的差异?
我们分析一下如下的代码
ini
public static void ij(int a[][], int rows, int columns)
{
long sum = 0L;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < columns; j++)
{
sum += a[i][j];
}
}
System.out.println(sum);
}
当循环a[0][0]时,它除了读取0,0索引的元素外,还会将邻近的(后续)元素也顺便读取进来(局部性原理) 凑读 64 个字节给它填满一个缓存行。所以不光读了0,0 就连 0,1到0,13也都读取进来了。
当读取0,1元素是也就是a[0][1]时,就不需要当内存中找了,因为已经缓存起来了所以直接找缓存就行了速度就比较快了
蓝色背景代表需要找内存,灰色背景代表不需要找内存直接找缓存,可以充分利用缓存来提高效率
直到最后的0,13,都直接找缓存中读取数据就可以了,速度很快
0~n的这一轮就循环读取完毕了,该下一行了就是 1,0了这时 缓存中是没有的就需要再到内存中读取了,同理读取一次后就会将邻近的元素都加载到缓存中去
总结:
我们发现,外层循环数组行,内层循环数组列 就能充分利用提升读取效率
下面再分析速度比较慢的情况,就是外层循环数组列,内层循环数组行
ini
public static void ji(int a[][], int rows, int columns)
{
long sum = 0L;
for (int i = 0; i < columns; i++)
{
for (int j = 0; j < rows; j++)
{
sum += a[j][i];
}
}
System.out.println(sum);
}
第一次将0,0的元素和邻近的元素到0,13的所有元素都读取到缓存了
但是第二次读取的是1,0因为i不变j变1 a[j][i] 也就是a[1][0]。这时缓存中并没有1,0 又要去读取内存了
第三次读取3,0。同样的缓存中没有需要找内存读取加载到缓存中。但是缓存是有限的,而缓存中的0,1 1,1 2,1 都没有用上 ,数据没用上接下来超过了缓存的最大值后它就会把旧的数据被新的数据覆盖掉
假设我们缓存最大值是7行数据,而我们需要遍历10行数据。此时超过缓存最大之后旧的数据就会被新的数据覆盖掉如下所示:
总结:
我们发现,外层循环数组列,内层循环数组行 会浪费掉每行缓存的数据 而 旧数据会被新数据覆盖因为缓存有限 这使得效率很低