在Java中,对二维数组先行后列(行优先)的求和效率通常显著高于先列后行(列优先)的求和。这背后的核心原因与计算机的内存架构和CPU缓存的工作机制密切相关。
🔍 深入理解性能差异的原因
1. 内存布局与访问模式
- 行优先存储:在Java中,二维数组 int[][]在内存中实际上是 “数组的数组” 。每一行(如 a[0])是一个独立的一维数组,在内存中是连续存储的。因此,当你按 a[i][j]顺序访问时,你是在连续的内存地址上顺序读取数据,就像在一条直路上前进。
- 列优先存储:而按 a[j][i]顺序访问时,为了获取下一列同一行的元素,你需要 “跳跃” 到内存中相隔很远的位置(距离约为“一行的大小”),就像在一条路上不断折返跑。这种非连续的访问模式效率很低。
2. CPU缓存与空间局部性
CPU缓存是解决CPU高速运算与内存低速读取之间矛盾的关键部件。其工作原理基于程序局部性原理,其中空间局部性是指:如果某个内存位置被访问,那么其附近的内存位置也很有可能在不久的将来被访问。
- 缓存行:CPU从内存读取数据时,并非只读取请求的一个字节,而是会一次性读取一个连续的块,称为缓存行,通常大小为 64 字节。对于 int类型(4字节),一个缓存行可以容纳约16个连续的数组元素。
- 行优先遍历的优势:当你访问 a[i][0]时,CPU不仅会加载这个元素,还会将其后的 a[i][1], a[i][2]等约15个相邻元素一同加载到高速缓存中。接下来访问 a[i][1]到 a[i][15]时,数据已经在高速缓存中,CPU可以直接读取,速度极快。这就是高缓存命中率。
- 列优先遍历的劣势:当你访问 a[0][j]后,下一个要访问的是 a[1][j],它位于内存中完全不同的另一行。这个新地址很大概率不在当前缓存行中,导致缓存未命中。CPU必须暂停当前工作,去速度慢得多的主内存中加载新的缓存行。这种频繁的缓存未命中严重拖慢了处理速度
下面这个例子:行优先遍历用了41ms,但是列优先遍历用了1028ms,相差几十倍。
@Test
public void test(){
int rows = 10000;
int cols = 10000;
int sum = 0;
int[][] array = new int[rows][cols];
long start = System.currentTimeMillis();
for (int i = 0; i < rows; i++){
for (int j = 0; j < cols; j++){
sum = sum + array[i][j];
}
}
long end = System.currentTimeMillis();
System.out.println("Time taken: " + (end - start) + " ms");
sum = 0;
long start1 = System.currentTimeMillis();
for (int j = 0; j < cols; j++){
for (int i = 0; i < rows; i++){
sum = sum + array[i][j];
}
}
long end1 = System.currentTimeMillis();
System.out.println("Time taken: " + (end1 - start1) + " ms");
}