為什麼處理排序陣列比未排序陣列快
今天在群裡看到一個有意思的問題——為什麼處理排序陣列比處理沒有排序的陣列要快,這個問題來源於 StackoverFlow(原文連結),雖然我看到程式碼略微知道原因,但是模模糊糊不夠清晰,搜了很多部落格也講的不夠明白,所以就自己來總結了。
首先來看一下問題,下面是很簡單的一段程式碼,隨機生成一些數字,對其中大於 128 的元素求和,記錄並列印求和所用時間。
import java.util.Arrays;
import java.util.Random;
public class Main
{
public static void main(String[] args)
{
// Generate data
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;
// !!! With this, the next loop runs faster
Arrays.sort(data);
// Test
long start = System.nanoTime();
long sum = 0;
for (int i = 0; i < 100000; ++i)
{
// Primary loop
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);
}
}
我的執行結果:分別在對陣列排序和不排序的前提下測試,在不排序時所用的時間比先排好序所用時間平均要多 10 ms。這不是巧合,而是必然的結果。
問題就出在那個if
判斷上面,在舊文順序、條件、迴圈語句的底層解釋中其實已經提到了造成這種結果的原因,只是舊文中沒有拿出具體的例子來說明。
為了把這個問題搞明白,需要先對流水線
有一定的瞭解。計算機是指令流驅動的,執行的是一個一個的指令,而執行一條指令,又要經過取指、譯碼、執行、訪存、寫回、更新
六個階段(不同的劃分方式所包含的階段不一樣)。
六個階段使用的硬體基本是不一樣的,如果一條指令執行完再去執行另一條指令,那麼在這段時間裡會有很多硬體處於空閒狀態,要使計算機的速度變快,那麼就不能讓硬體停下來,所以有了流水線技術。
流水線技術透過將指令重疊來實現幾條指令並行處理,下圖表示的是三階段指令時序,即把一個指令分為三個階段。在第一條指令的 B 階段,A 階段相關的硬體是空閒的,於是可以將第二條指令的 A 階段提前操作。
很明顯,這種設計大幅提高了指令執行的效率,聰明的你可能發現問題了,要是不知道下一條指令是什麼怎麼辦,那提前的階段也就白乾了,那樣流水線不就失效了?沒錯,這就是導致開篇問題的原因。
讓流水線出問題的情況有三種:1、資料相關
,後一條指令需要用到前一條指令的運算結果;2、控制相關
,比如無條件跳轉,跳轉的地址需要在譯碼階段才能知道,所以跳轉之後已經被取出的指令流水就需要清空;3、結構相關
,由於一些指令需要的時鐘週期長(比如浮點運算等),長時間佔用硬體,導致之後的指令無法進入譯碼等階段,即它們在爭用同一套硬體。
程式碼中的if (data[c] >= 128)
翻譯成機器語言就是跳轉指令,處理器事先並不知道要跳轉到哪個分支,那難道就等知道了才開始下一條指令的取指工作嗎?處理器選擇了假裝知道會跳轉到哪個分支(不是謙虛,是真的假裝知道),如果猜中了是運氣好,而沒有猜中那就浪費一點時間重新來幹。
沒有排序的陣列,元素是隨機排列的,每次data[c] >= 128
的結果也是隨機的,前面的經驗就不可參考,所以下一次執行到這裡理論上還是會有 50% 的可能會猜錯,猜錯了肯定就需要花時間來修改犯下的錯誤,自然就會浪費更多的時間。
對於排好序的陣列,開始幾次也需要靠猜,但是猜著猜著發現有規律啊,每次都是往同一個分支跳轉,所以以後基本上每次都能猜中,當遍歷到與 128 分界的地方,才會出現猜不中的情況,但是猜幾次之後,發現這又有規律啊,每次都是朝著另外一個相同分支走的。
雖然都會猜錯,但是在排好序的情況下猜錯的機率遠遠小於未排序時的機率,最終呈現的結果就是處理排序陣列比未排序陣列快,其原因就是流水線發生了大量的控制相關現象,下面通俗一點,加深一下理解。
遠在他方心儀多年的姑娘突然告訴你,其實她也喜歡你,激動的你三天三夜睡不著覺,決定開車前往她的城市,要和她待在一起,但是要去的路上有很多很多岔路,你只能使用的某某地圖導航,作為老司機並且懷著立馬要見到愛人心情的你,開車超快,什麼樣罰單都不在乎了。
地圖定位已經跟不上你的速度了,為了儘快到達,遇到岔路你都是隨機選一條路前進,遺憾的是,自己的選擇不一定對(我們假設高速可以回退),走錯路了就要重新回到分岔點,這就對應著未排序的情況。
現在岔路是有規律的,告訴你開始一直朝著一邊走,到某個地點後會一直朝著另一邊走,你只需要花點時間去探索一下開始朝左邊還是右邊,到了中間哪個地點會改變方向就可以了,相比之下就能節省不少時間了,儘快見到自己的愛人,這對應著排好序的情況。
最後的故事改編自兩個人的現實生活,一位是自己最好的朋友之一,談戀愛開心的睡不著覺;另一位是微信上的一位好友,為了對方從北京裸辭飛到了深圳。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555494/viewspace-2222267/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 為什麼處理排序的陣列要比非排序的快排序陣列
- 為什麼處理有序陣列比無序陣列快?陣列
- 為什麼?為什麼?Java處理排序後的陣列比沒有排序的快?想過沒有?Java排序陣列
- 分支預測:為什麼有序陣列比無序陣列快?陣列
- ***PHP陣列排序+php二維陣列排序方法(PHP比較器)PHP陣列排序
- 陣列排序陣列排序
- 陣列二:使用陣列可變函式為陣列排序陣列函式排序
- 陣列的操作處理與陣列元素的氣泡排序 (轉)陣列排序
- 陣列的排序陣列排序
- 物件陣列排序物件陣列排序
- JavaScript 陣列排序JavaScript陣列排序
- js陣列排序JS陣列排序
- 多維陣列排序陣列排序
- 陣列氣泡排序陣列排序
- 陣列選擇排序陣列排序
- js陣列排序整理JS陣列排序
- javascript 陣列快速排序JavaScript陣列排序
- 陣列多重排序陣列排序
- c# 陣列排序C#陣列排序
- 二維陣列排序陣列排序
- 記一次陣列操作:陣列 A 根據陣列 B 排序陣列排序
- 陣列排序函式-php陣列函式(一)陣列排序函式PHP
- 二位陣列排序陣列排序
- 陣列排序的實現陣列排序
- php 二維陣列排序PHP陣列排序
- c++陣列排序插入C++陣列排序
- 二維陣列行排序陣列排序
- js:陣列自定義排序JS陣列排序
- 陣列排序(從小到大)陣列排序
- JavaScript陣列隨機排序JavaScript陣列隨機排序
- 陣列排序的測試陣列排序
- 陣列先去重,後排序陣列排序
- PHP 多維陣列排序PHP陣列排序
- 為什麼對陣列排序讓Python迴圈執行更快陣列排序Python
- java之陣列的索引,排序以及二維陣列Java陣列索引排序
- 陣列的去重和排序陣列排序
- PHP 陣列排序(複雜字串)PHP陣列排序字串
- js陣列排序和打亂JS陣列排序