對一段代碼的性能分析

先看這麼一段代碼:

/**
 * @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

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章