对一段代码的性能分析

先看这么一段代码:

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

欢迎关注公众号
​​​​​​在这里插入图片描述

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