爲何?爲何?Java處理排序後的數組比沒有排序的快?想過沒有?

先看再點贊,給本身一點思考的時間,微信搜索【 沉默王二】關注這個有顏值卻僞裝靠才華苟且的程序員。
本文  GitHub  github.com/itwanger 已收錄,裏面還有我精心爲你準備的一線大廠面試題。

今天週日,沒什麼重要的事情要作,因而我早早的就醒來了。看了一會渡邊淳一的書,心裏逐漸感到平靜——心情不佳的時候,書好像是最好的藥物。心情平靜了,就須要作一些更有意義的事情——逛技術網站,學習精進。java

Stack Overflow 是我最喜歡逛的一個網站,它是我 Chrome 瀏覽器的第一個書籤。裏面有不少不少經典的問題,其中一些回答,剖析得深刻我心。就好比說這個:「爲何處理排序後的數組比沒有排序的快?」git

毫無疑問,直觀印象裏,排序後的數組處理起來就是要比沒有排序的快,甚至不須要理由,就好像咱們知道「夏天吃冰激凌就是爽,冬天穿羽絨服就是暖和」同樣。程序員

但本着「知其然知其因此然」的態度,咱們確實須要去搞清楚究竟是爲何?github

來看一段 Java 代碼:面試

/**
 * @author 沉默王二,一枚有趣的程序員
 */
public class SortArrayFasterDemo {
    public static void main(String[] args) {
        // 聲明數組
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c) {
            data[c] = rnd.nextInt() % 256;
        }

        // !!! 排序後,比沒有排序要快
        Arrays.sort(data);

        // 測試
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i)
        {
            // 循環
            for (int c = 0; c < arraySize; ++c)
            {
                if (data[c] >= 128) {
                    sum += data[c];
                }
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}

這段代碼很是簡單,我來解釋一下:數組

  • 聲明一個指定長度(32768)的數組。
  • 聲明一個 Random 隨機數對象,種子是 0;rnd.nextInt() % 256 將會產生一個餘數,餘數的絕對值在 0 到 256 之間,包括 0,不包括 256,多是負數;使用餘數對數組進行填充。
  • 使用 Arrays.sort() 進行排序。
  • 經過 for 循環嵌套計算數組累加後的結果,並經過 System.nanoTime() 計算先後的時間差,精確到納秒級。

我本機的環境是 Mac OS,內存 16 GB,CPU Intel Core i7,IDE 用的是 IntelliJ IDEA,排序後和未排序後的結果以下:瀏覽器

排序後:2.811633398
未排序:9.41434346微信

時間差仍是很明顯的,對吧?未排序的時候,等待結果的時候讓我有一種擔憂:何時結束啊?不會結束不了吧?dom

讀者朋友們有沒有玩過火炬之光啊?一款很是經典的單機遊戲,每個場景都有一副地圖,地圖上有不少分支,但只有一個分支能夠通往下一關;在沒有刷圖以前,地圖是模糊的,玩家並不知道哪一條分支是正確的。學習

若是僥倖跑的是一條正確的分支,那麼很快就能到達下一關;不然就要往回跑,尋找正確的那條分支,須要花費更多的時間,但同時也會收穫更多的經驗和聲望。

做爲一名玩過火炬之光好久的老玩家,幾乎每一幅地圖我都刷過不少次,刷的次數多了,地圖差很少就刻進了個人腦殼,即使是一開始地圖是模糊的,我也能憑藉經驗和直覺找到最正確的那條分支,就省了不少折返跑的時間。

讀者朋友們應該注意到了,上面的代碼中有一個 if 分支——if (data[c] >= 128),也就是說,若是數組中的值大於等於 128,則對其進行累加,不然跳過。

那這個代碼中的分支就好像火炬之光中的地圖分支,若是處理器可以像我同樣提早預判,那累加的操做就會快不少,對吧?

處理器的內部結構我是不懂的,但它應該和個人大腦是相似的,遇到 if 分支的時候也須要停下來,猜一猜,到底要不要繼續,若是每次都猜對,那顯然就不須要折返跑,浪費時間。

這就是傳說中的分支預測!

我須要刷不少次圖才能正確地預測地圖上的路線,處理器須要排序才能提升判斷的準確率

計算機發展了這麼多年,已經變得很是很是聰明,對於條件的預測一般能達到 90% 以上的命中率。可是,若是分支是不可預測的,那處理器也無能爲力啊,對不對?

排序後花費的時間少,未排序花費的時間多,罪魁禍首就在 if 語句上。

if (data[c] >= 128) {
    sum += data[c];
}

數組中的值是均勻分佈的(-255 到 255 之間),至因而怎麼均勻分佈的,咱們暫且無論,反正由 Random 類負責。

爲了方便講解,咱們暫時忽略掉負數的那一部分,從 0 到 255 提及。

來看通過排序後的數據:

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT

N 是小於 128 的,將會被 if 條件過濾掉;T 是將要累加到 sum 中的值。

再來看未排序的數據:

data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, 133, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T,   N  ...

       = TTNTTTTNTNNTTTN ...

徹底沒有辦法預測。

對比事後,就能發現,排序後的數據在遇到分支預測的時候,可以輕鬆地過濾掉 50% 的數據,對吧?是有規律可循的。

那假如說不想排序,又想節省時間,有沒有辦法呢?

若是你直接問個人話,我確定毫無辦法,兩手一攤,一副無奈臉。不過,Stack Overflow 以上帝視角給出了答案。

把:

if (data[c] >= 128) {
    sum += data[c];
}

更換爲:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

經過位運算消除了 if 分支(並不徹底等同),但我測試了一下,計算後的 sum 結果是相同的。

/**
 * @author 沉默王二,一枚有趣的程序員
 */
public class SortArrayFasterDemo {
    public static void main(String[] args) {
        // 聲明數組
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random();
        for (int c = 0; c < arraySize; ++c) {
            data[c] = rnd.nextInt() % 256;
        }

        // 測試
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i)
        {
            // 循環
            for (int c = 0; c < arraySize; ++c)
            {
                if (data[c] >= 128) {
                    sum += data[c];
                }
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);

        // 測試
        long start1 = System.nanoTime();
        long sum1 = 0;

        for (int i = 0; i < 100000; ++i)
        {
            // 循環
            for (int c = 0; c < arraySize; ++c)
            {
                int t = (data[c] - 128) >> 31;
                sum1 += ~t & data[c];
            }
        }

        System.out.println((System.nanoTime() - start1) / 1000000000.0);
        System.out.println("sum1 = " + sum1);
    }
}

輸出結果以下所示:

8.734795196
sum = 156871800000
1.596423307
sum1 = 156871800000

數組累加後的結果是相同的,但時間上仍然差得很是多,這說明時間確實耗在分支預測上——若是數組沒有排序的話。

最後,不得不說一句,大神級程序員不愧是大神級程序員,懂得位運算的程序員就是屌。

建議還在讀大學的讀者朋友多讀一讀《計算機操做系統原理》這種涉及到底層的書,對成爲一名優秀的程序員頗有幫助。畢竟大學期間,學習時間充分,社會壓力小,可以作到心無旁騖,加油!


我是沉默王二,一枚有顏值卻僞裝靠才華苟且的程序員。關注便可提高學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,奧利給🌹

注:若是文章有任何問題,歡迎絕不留情地指正。

若是你以爲文章對你有些幫助,歡迎微信搜索「沉默王二」第一時間閱讀;本文 GitHub github.com/itwanger 已收錄,歡迎 star。

相關文章
相關標籤/搜索