先看這麼一段代碼:
/**
* @author Dongguabai
* @Description
* @Date 創建於 2020-06-02 23:27
*/
public class ArrayTest {
private static StopWatch sw = new StopWatch("Test");
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1000);
int[][] a = new int[10000][10000];
sw.start();
loop1(a);
sw.stop();
System.out.println(sw.prettyPrint());
}
private static void loop1(int[][] a) {
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a[i].length; j++) {
a[i][j] = j;
}
}
}
private static void loop2(int[][] a) {
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a[i].length; j++) {
a[j][i] = j;
}
}
}
}
執行 loop1()
函數,結果爲:
StopWatch 'Test': running time = 49523606 ns
---------------------------------------------
ns % Task name
---------------------------------------------
049523606 100%
再執行 loop2()
函數,結果爲:
StopWatch 'Test': running time = 1539952133 ns
---------------------------------------------
ns % Task name
---------------------------------------------
1539952133 100%
可以發現,明顯 loop2()
函數的執行時間是 loop1()
函數的執行時間的 30 倍。
首先兩個函數的循環次數是相同的,雖然最終生成的二維數組的元素內容是不一樣的,但都是賦值操作,這個是不影響的。
那麼爲什麼執行效率差距這麼大呢。首先要明確一點,多維數組的初始化是建立在一維數組的初始化的基礎上的,無論是一維數組還是多維數組都是線性連續存儲的。通過下面這段代碼可以證明:
#include <stdio.h>
int a[10][10];
int main(void)
{
int i,j;
for (i = 0; i < 10; i++) {
for (j = 0; j < 10; j++) {
* (&a[i][j]) = j;
printf("[%d]-----[%p]\r\n",a[i][j],&a[i][j]);
}
}
}
執行結果:
➜ temp make array
make: `array' is up to date.
➜ temp ./array
[0]-----[0x106cbb010]
[1]-----[0x106cbb014]
[2]-----[0x106cbb018]
[3]-----[0x106cbb01c]
[4]-----[0x106cbb020]
[5]-----[0x106cbb024]
[6]-----[0x106cbb028]
[7]-----[0x106cbb02c]
[8]-----[0x106cbb030]
[9]-----[0x106cbb034]
[0]-----[0x106cbb038]
[1]-----[0x106cbb03c]
[2]-----[0x106cbb040]
[3]-----[0x106cbb044]
[4]-----[0x106cbb048]
[5]-----[0x106cbb04c]
[6]-----[0x106cbb050]
[7]-----[0x106cbb054]
[8]-----[0x106cbb058]
[9]-----[0x106cbb05c]
[0]-----[0x106cbb060]
[1]-----[0x106cbb064]
[2]-----[0x106cbb068]
[3]-----[0x106cbb06c]
[4]-----[0x106cbb070]
[5]-----[0x106cbb074]
[6]-----[0x106cbb078]
[7]-----[0x106cbb07c]
[8]-----[0x106cbb080]
[9]-----[0x106cbb084]
[0]-----[0x106cbb088]
...
可以看到這裏 int 是 32 位的,地址值是連續的,每次加 4。
也就是說上面的二維數組我們可以簡單看成是這樣分佈的:
0,1,2,3,4,5,6,7,8,9...0,1,2,3,4,5,6,7,8,9...0,1,2,3,4......
兩個函數的遍歷方式如下(這裏借用了網上的一張方格圖片,具體鏈接見文末):
接下來再看一個概念:局部性原理。引用《計算機操作系統 第三版》中的內容:
早在 1968 年,Denning.P 就曾指出:程序在執行時將呈現出局部性規律,即在一較短 的時間內,程序的執行僅侷限於某個部分;相應地,它所訪問的存儲空間也侷限於某個區域。
侷限性還表現在下述兩個方面:
(1) 時間侷限性。如果程序中的某條指令一旦執行,則不久以後該指令可能再次執行; 如果某數據被訪問過,則不久以後該數據可能再次被訪問。產生時間侷限性的典型原因是 由於在程序中存在着大量的循環操作。
(2) 空間侷限性。一旦程序訪問了某個存儲單元,在不久之後,其附近的存儲單元也將 被訪問,即程序在一段時間內所訪問的地址,可能集中在一定的範圍之內,其典型情況便 是程序的順序執行。
還有一個就是 CPU 是有三級緩存的:
一級緩存速度是最快的。那麼根據局部性原理,即程序在執行時將呈現出局部性規律,在一較短的時間內, 程序的執行僅侷限於某個部分。第一種遍歷方式是與數組的內存地址連續分配“方向”是一致的,符合局部性原理,所以第一種方式明顯緩存命中率是比第二種高的,而如果緩存未命中,則 CPU 需要到內存中讀取數據。這也就解釋了爲什麼 loop1()
函數執行效率比 loop2()
效率高。
References
-
《計算機操作系統 第三版》
歡迎關注公衆號