原標題:特斯拉遇上 CPU:程序員的心思你別猜

點擊上圖,查看課程教學大綱

18世紀流水線的誕生帶來了製造技術的變革,人類當今擁有琳琅滿目物美價廉的商品和流水線技術的發明密不可分, 因此當你喝着可樂、吹着空調、坐在特斯拉里拿着智能手機刷這篇文章時需要感謝流水線技術

一段有趣的代碼

有這樣一段代碼:

for( intk = 0; k < 10000; k++){ for( inti = 0; i < arr.size; i++) { if(arr[i] > 256) sum += arr[i];}}

這段代碼非常簡單,給定一個數組,計算所有大於256 的元素之和,重複計算 10000 遍。

這段代碼本身平淡無奇,但有趣的是: 如果這個數組是有序的,那麼這段代碼的運行速度會比處理無序數組快將近 10 倍(不同的機器、CPU架構可能會稍有差異)。

可這是爲什麼呢?這和製造業使用的流水線又有什麼關係呢?且聽我慢慢道來。

流水線技術的誕生

1769年,英國人喬賽亞·韋奇伍德開辦了一家陶瓷工廠,這家工廠生產的陶瓷乏善可陳,但其內部的管理方式極具創新性,傳統的方法都是由製陶工專人來完成,但韋奇伍德研究後將整個製陶工藝流程分成了 幾十道工序,每一道工序都交給專人完成,這樣傳統的製陶人不復存在,這便是工業流水線最早的雛形。

發揚光大

雖然流水線技術可以說是英國人發明的,但發揚光大的卻是美國人,這便是福特與T型車。

20世紀初,福特將流水線技術應用到汽車的批量生產,效率得到千倍提高, 使得汽車這種奢侈品開始能夠爲大衆消費,深刻影響了現代社會的方方面面,注意上圖中一輛車的價格。。。

100 年後又一個美國人攜帶他的時尚電動車再一次席捲全球,這就是特斯拉。

接下來我們仔細看一下流水線技術。

特斯拉與流水線

假設組裝一輛特斯拉需要經過:組裝車架、安裝引擎、安裝電池、檢驗四道工序,同時假設每個步驟需要 20 分鐘,因此如果所有工序都由一個組裝站點來完成,那麼組裝一輛特斯拉需要80分鐘。

但如果每個步驟都交給一個特定站點來組裝的話就不一樣了,此時生產一輛車的時間依然是80分鐘,但這只是第一輛車所需要的時間,此後工廠可以每20分鐘就交付一輛特斯拉。

注意,流水線並沒有減少組裝一輛車的時間,只是增加了工廠的吞吐能力。

流水線技術的使用極大增加了工廠交付車輛的效率。

CPU 與超級工廠

其實 CPU 本身也是一座超級工廠。

只不過CPU這座工廠生產的不是特斯拉,而是機器指令。

工廠內部有流水線極大提高了生產效率,CPU 沒有理由不擁有。

你可以想象一下, 不管你現在看這篇文章用的是PC 還是智能手機,其內部的 CPU 都有一條複雜度不亞於特斯拉超級工廠的流水生產線

如果我們把CPU處理的一條機器指令當做一輛特斯拉的話,那麼對於現代CPU這座超級工廠來說, 一秒鐘的時間內可以交付數十億量特斯拉,效率完爆任何當今製造界的工業流水線, CPU 纔是一座名副其實的超級工廠

如果特斯拉超級工廠也如 CPU 一般高效的話,特斯拉可能比現在的自行車都要便宜,地球人民人手一輛特斯拉不成問題,算上外星人也不成問題。

機器指令與流水線

實際上說 CPU 生產機器指令是不正確的, CPU 其實不是在生產機器指令而是在處理機器指令,生產機器指令的是編譯器,CPU需要處理機器指令以此來指揮整個計算機系統工作。

同生產一輛特斯拉需要四道工序一樣,處理一條機器指令大體上也可以分爲四個步驟:取指、譯碼、執行、回寫,這幾個階段分別由特定的硬件來完成 (注意,真實 CPU 內部可能會將執行一條指令分解爲數十個階段)。

怎麼樣,是不是和超級工廠生產特斯拉沒什麼區別, 當今CPU用每秒處理數十億機器指令的能力驅動着智能手機好讓你流暢的刷公衆號、短視頻、刷微博、刷知乎,這裏,流水線技術功不可沒。

當 if 遇到流水線

實際上 CPU 內部的流水線和現實中的並不完全一樣。

程序員在代碼中編寫的 if 語句一般會被編譯器翻譯成一條跳轉指令,

if 語句其實起到一種 分支的作用,如果條件成立則需要執行if內部的邏輯,否則不執行;因此 跳轉指令會依賴自身的執行結果來決定到底要不要跳轉,這會對流水線產生影響。

有的同學可能不明白,這能產生什麼影響呢?

現在,讓我們仔細觀察一下特斯拉流水線,你會發現 當前一輛車還沒有完全製造完成時後一輛車就已經進入到流水線了

對於CPU來說道理是一樣的,當一條跳轉指令還沒有完成時後面的指令就需要進入到流水線,因此問題來了:

跳轉指令需要依賴自身的執行結果來決定到底要不要跳轉,那麼在跳轉指令沒有執行完的情況下 CPU 怎麼知道後面哪個分支的指令能進入到流水線呢

CPU 能預測未來嗎?

預測未來

對此 CPU 當然是不知道的。

那麼該怎麼辦呢?

很簡單,一個字,

你沒有看錯,CPU 會猜一下 if 語句可能會走哪個分支,如果猜對了流水線照常繼續,如果猜錯了,對不起, 流水線上已經執行的後續指令全部作廢,因此我們可以看到如果CPU猜錯了會有性能損耗。

現代 CPU 將“猜”的這個過程稱爲 分支預測

當然,CPU 中的分支預測並不是簡單的拋硬幣式的隨機瞎猜,而且有特定策略,比如可能會基於執行跳轉指令的歷史去進行預測等等。

知道了分支預測就可以解釋文章開頭的問題了。

程序員的心思你別猜

現在我們知道,程序員編寫的 if 語句對應的是跳轉指令:

if(arr[i] >= 256) { sum+= arr[i]; }

CPU 在執行完跳轉指令之前必須決定後續哪個分支的指令會進入到流水線,猜對了流水線照常進行,猜錯了有性能損耗。

那麼如果一個數組是有序的:

而如果一個數組是無序的:

你覺得哪種更好猜一些?

如果你給CPU一個無序數組,那麼 Arr[i] 是否大於256 基本上就是隨機的, 對於隨機事件,不要說CPU的分支預測,任何其它預測手段都將無效,否則這就不是隨機事件了

如果 CPU 猜的不對,那麼流水線上的後續指令將作廢,這就解釋了爲什麼處理有序數組要比處理無序數組性能好了,因爲在數組有序的情況下,CPU 的分支預測幾乎不會猜錯,流水線上的指令不會被頻繁作廢。

這對程序員的啓示就是:如果你編寫了 if 語句,那麼 你最好讓 CPU 大概率能猜對

有的同學看到這裏,可能會覺得每一條 if 語句都性能低下,恨不得從此不再寫if else,真的是這樣嗎?

編寫 If else時需要注意什麼

實際上如果你編寫的if語句沒有位於對性能要求很高的核心代碼部分,那麼分支預測失敗這種問題無需關心。

實際上現代 CPU 的分支預測是很聰明的,對於非核心部分的if 語句分支預測失敗帶來的性能損失可以忽略不計。

但是對於文章開頭提到的代碼,程序的大部分時間都用在了 for 循環中,這時你就要注意了, 當然前提還是這段代碼對時間要求非常嚴苛,否則你也沒必要爲了這點性能去優化。

好奇的同學可能會問,如果給定的數組是無序的,那麼上面提到的這段該怎麼優化呢?

性能優化

實際上非常簡單,只需要移除 if 語句就可以,該怎麼移除呢?

沒有 if 語句的話, 那麼 sum 每次都必須加上一個數,如果arr[i]比256大,那麼 sum 加上差值,否則 sum 加 0即可,這樣就消除了if 判斷。

我們計算arr[i] - 256的值,並將其向右移動31位:

(arr[i] - 256) >> 31

這樣得到的數不是0 (0x00000000),就是 -1 (0xffffffff),然後我們對其取反,再次與上 arr[i] 即可:

sum += ~((arr[i] - 256) >> 31) & arr[i];

也就是說如果arr[i] - 256 大於0 的話那麼差值會與上 0xffffffff,其結果就是保持不變,否則會與上0,其結果就是sum會加上0,這樣就不需要 if 判斷了。

利用位運算,即使數組是無序的也不會有性能問題, 代價就是代碼可讀性會降低很多,這裏,我們再一次看到 天下沒有免費的午餐

總結

雖然 CPU 體積很小,只有指甲那麼大,但 CPU 可能是人類有史以來建造過的最複雜的東西,在這裏實現了很多有趣的功能,程序員只有徹底理解 CPU 才能更好的利用這些功能編寫性能優異的程序。

希望這篇對大家理解 CPU 有所幫助。

相關文章