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


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

 

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

 

有如下兩段代碼:

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];
      }
}

 

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

第二段代碼執行效率比第一段代碼低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循環。

 

當我們擼了如下一行代碼的時候:

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 全部失效,由此帶來的性能是極低的。

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM