為什麼?為什麼?Java處理排序後的陣列比沒有排序的快?想過沒有?
先看再點贊,給自己一點思考的時間,微信搜尋【沉默王二】關注這個有顏值卻假裝靠才華苟且的程式設計師。
本文 GitHub github.com/itwanger 已收錄,裡面還有我精心為你準備的一線大廠面試題。
今天週日,沒什麼重要的事情要做,於是我早早的就醒來了。看了一會渡邊淳一的書,內心逐漸感到平靜——心情不佳的時候,書好像是最好的藥物。心情平靜了,就需要做一些更有意義的事情——逛技術網站,學習精進。
Stack Overflow 是我最喜歡逛的一個網站,它是我 Chrome 瀏覽器的第一個書籤。裡面有很多很多經典的問題,其中一些回答,剖析得深入我心。就比如說這個:“為什麼處理排序後的陣列比沒有排序的快?”
毫無疑問,直觀印象裡,排序後的陣列處理起來就是要比沒有排序的快,甚至不需要理由,就好像我們知道“夏天吃冰激凌就是爽,冬天穿羽絨服就是暖和”一樣。
但本著“知其然知其所以然”的態度,我們確實需要去搞清楚到底是為什麼?
來看一段 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
時間差還是很明顯的,對吧?未排序的時候,等待結果的時候讓我有一種擔心:什麼時候結束啊?不會結束不了吧?
讀者朋友們有沒有玩過火炬之光啊?一款非常經典的單機遊戲,每一個場景都有一副地圖,地圖上有很多分支,但只有一個分支可以通往下一關;在沒有刷圖之前,地圖是模糊的,玩家並不知道哪一條分支是正確的。
如果僥倖跑的是一條正確的分支,那麼很快就能到達下一關;否則就要往回跑,尋找正確的那條分支,需要花費更多的時間,但同時也會收穫更多的經驗和聲望。
作為一名玩過火炬之光很久的老玩家,幾乎每一幅地圖我都刷過很多次,刷的次數多了,地圖差不多就刻進了我的腦袋,即便是一開始地圖是模糊的,我也能憑藉經驗和直覺找到最正確的那條分支,就省了很多折返跑的時間。
讀者朋友們應該注意到了,上面的程式碼中有一個 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
陣列累加後的結果是相同的,但時間上仍然差得非常多,這說明時間確實耗在分支預測上——如果陣列沒有排序的話。
最後,不得不說一句,大神級程式設計師不愧是大神級程式設計師,懂得位運算的程式設計師就是屌。
建議還在讀大學的讀者朋友多讀一讀《計算機作業系統原理》這種涉及到底層的書,對成為一名優秀的程式設計師很有幫助。畢竟大學期間,學習時間充分,社會壓力小,能夠做到心無旁騖,加油!
我是沉默王二,一枚有顏值卻假裝靠才華苟且的程式設計師。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,奧利給