(原)一段看似美麗的for循環,背後又隱藏着什麼

以前很長一段時間,潛心修煉彙編,專門裝了一個dos7,慢慢玩到win32彙編,再到linux的AT&A彙編,嘗試寫mbr的時候期間好幾回把centos弄的開不了機,又莫名其妙的修好了,現在最大的感觸就是:球莫名堂,還不如寫JAVAlinux

 

對於比較高層的語言來講,都不會太在乎底層是如何運做的,這是個好事,也是個壞事,好事是不用關心底層的繁瑣的事情,只需聚焦到業務實現,壞處就是出現比較嚴重的問題難以排錯,很容易出現看起來很漂亮但就是性能很渣的代碼。程序員

 

有以下兩段代碼:編程

for (int i = 0; i < longs.length; i++) {
    for (int j = 0; j < longs[i].length; j++) {
         Long k = longs[i][j];
    }
}

 

for (int i = 0; i < longs.length; i++) {
      for (int j = 0; j < longs[i].length; j++) {
            Long k1 = longs[j][i];
      }
}

 

看起來長的同樣是否是?兩段代碼看起來都沒啥問題是吧,相信不少人都或多或少的擼過這樣的兩段代碼,可是這兩段代碼的運行效率比較是:centos

第二段代碼執行效率比第一段代碼低300倍數組

 

完整的測試代碼:緩存

public class RepeatIterator {

    private static final int ARRAY_SIZE = 10240;
    private Long[][] longs = new Long[ARRAY_SIZE][ARRAY_SIZE];

    public static void main(String[] args) {
        new RepeatIterator().iteratorByRow();
        new RepeatIterator().iteratorByColumn();
    }

    private void iteratorByRow() {long start = System.currentTimeMillis();
        for (int i = 0; i < longs.length; i++) {
            for (int j = 0; j < longs[i].length; j++) {
                Long k = longs[i][j];
            }
        }
        System.out.println("iterator by row:" + (System.currentTimeMillis() - start));
    }

    private void iteratorByColumn() {long start = System.currentTimeMillis();
        for (int i = 0; i < longs.length; i++) {
            for (int j = 0; j < longs[i].length; j++) {
                Long k1 = longs[j][i];
            }
        }
        System.out.println("iterator by column:" + (System.currentTimeMillis() - start));
    }
}

 

執行結果:服務器

iterator by row:6
iterator by column:1737

Process finished with exit code 0

 

代碼爲什麼執行緩慢,機器爲什麼頻繁卡死,服務器爲什麼屢屢宕機,看似美麗的代碼背後又隱藏着什麼,這一切的背後,是程序員人性的扭曲仍是道德的淪喪,是碼農憤怒的爆發仍是飢渴的無奈,讓咱們跟隨鏡頭走進計算機的心裏世界,解刨那一段小巧的for循環。dom

 

當咱們擼了以下一行代碼的時候:性能

private static final int ARRAY_SIZE = 10240;
private Long[][] longs = new Long[ARRAY_SIZE][ARRAY_SIZE];

 

在計算機的內存裏面是以下分佈(至少在個人計算機裏面是這樣分佈的):測試

 

能夠明確的看到在內存中的數組大小爲10240,也就是咱們定義的大小,以及他的的地址(這並非實際的物理地址,8086裏面是段的偏移地址,i386裏面是分頁地址),可是當遍歷該數組的時候,並非直接從內存地址中取出這些數據,由於內存對於cpu來講:太慢了。爲了充分利用cpu的效率,因而人們設計出了cpu緩存,目前已經存在三級cpu緩存,而不一樣的緩存意義並不同,特別是寫多核編程的時候,若是對cpu緩存的理解不到位,很容易死在僞共享裏面。

 

一個具備三級緩存的圖示以下:

 

其中1級緩存並非一塊緩存,而是2個部分,分別爲代碼緩存和數據緩存,1級和2級緩存爲單個cpu獨享,其餘cpu不能修改到裏面的數據,而3級緩存,則爲多個cpu共享,而cpu僞共享,也是發生在這個位置,程序定義的數據,大多時候緩存在3級緩存,緩存也是行導向存儲,經過以下方式能夠查看一行緩存可以存儲多少數據:

 

cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64

 

64表明64個字節,一個Long對象的長度是8個字節,那麼64個字節能夠緩存8個Long,數組在內存中是一片連續的地址空間(物理也許不必定,但邏輯地址必定連續),這就意味着若是定義個一個8個長度的Long數組,當訪問第一個數組元素被添加到緩存的時候,那麼其餘7個順帶的0消耗的就加載到了緩存中,這時候若是訪問數組,那麼速度是最高效的。也就是意味着,要充分利用緩存的特性,數據已定要按照行訪問,不然會形成cache miss,這時候會從內存中獲取數據,而且計算是否須要將其緩存,會極大的下降速度。

在上面的例子中,定義的二維數組,當使用第一種方式訪問的時候,會發生以下狀況:

1.訪問第一行第一個元素,若是緩存中不存在(cache miss),從內存中獲取,而且將其相鄰的元素同時緩存。

2.訪問第一行第二個元素,直接緩存取出(cache命中)

舉個例子:

public class CacheLoad {

    private static final int ARRAY_SIZE = 10240;
    private Long[][] longs = null;
    public static void main(String[] args) {
        new CacheLoad().iterator();
        new CacheLoad().iterator();
    }

    private void iterator() {
        if (longs == null) {
            longs = new Long[ARRAY_SIZE][ARRAY_SIZE];
            for (int i = 0; i < longs.length; i++) {
                for (int j = 0; j < longs[i].length; j++) {
                    longs[i][j] = new Random().nextLong();
                }
            }
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < longs.length; i++) {
            for (int j = 0; j < longs[i].length; j++) {
                Long k = longs[i][j];
            }
        }
        System.out.println("iterator:" + (System.currentTimeMillis() - start));
    }
}

 

iterator:5
iterator:1

Process finished with exit code 0

 

第二次的查詢速度理論(實際可能會大於,由於cpu線程切換,訪問過程當中可能被系統其餘資源搶佔cpu)是小於等於第一次,由於會減小將第一個元素緩存的時間,另外並非所有的數據都會盡緩存,這不是程序所能控制。

 

當咱們採起第二種方式訪問的時候,會發生以下狀況:

1.訪問第一行第一個元素,若是緩存中不存在(cache miss),從內存中獲取,而且將其相鄰的元素同時緩存。

2.訪問第二行第一個元素,若是緩存中不存在(cache miss),從內存中獲取,而且將其相鄰的元素同時緩存。

。。。。。。。

由此能夠看到,採用第二種方式訪問數組的時候,很大的機率會形成cache miss,第二條cache沖掉第一條cache,極端狀況是每次都miss,而且不管執行多少次,始終會miss,例如:

public class CacheLoad {

    private static final int ARRAY_SIZE = 10240;
    private Long[][] longs = new Long[ARRAY_SIZE][ARRAY_SIZE];;
    public static void main(String[] args) {
        new CacheLoad().iterator();
        new CacheLoad().iterator();
        new CacheLoad().iterator();
        new CacheLoad().iterator();
    }

    private void iterator() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < longs.length; i++) {
            for (int j = 0; j < longs[i].length; j++) {
                Long k = longs[j][i];
            }
        }
        System.out.println("iterator:" + (System.currentTimeMillis() - start));
    }
}

 

iterator:1658
iterator:1697
iterator:1915
iterator:1728

Process finished with exit code 0

能夠看到不管執行多少次,速度並不會所以變快,能夠看見幾本cache 所有失效,由此帶來的性能是極低的。

 

擼代碼的時候,且擼且當心。。。

相關文章
相關標籤/搜索