過去兩講,我爲你講解了經過增長資源、停頓等待以及主動轉發數據的方式,來解決結構冒險和數據冒險問題。對於結構冒險,因爲限制來自於同一時鐘週期不一樣的指令,
要訪問相同的硬件資源,解決方案是增長資源。對於數據冒險,因爲限制來自於數據之間的各類依賴,咱們能夠提早把數據轉發到下一個指令。緩存
可是即使綜合運用這三種技術,咱們仍然會遇到不得不停下整個流水線,等待前面的指令完成的狀況,也就是採用流水線停頓的解決方案。好比說,上一講裏最後給你的例子,
即便咱們進行了操做數前推,由於第二條條加法指令依賴於第一條指令從內存中獲取的數據,咱們仍是要插入一次NOP的操做。架構
那咱們能不能讓後面沒有數據依賴的指令,在前面指令停頓的時候先執行呢?併發
答案固然是能夠的。畢竟,流水線停頓的時候,對應的電路閒着也是閒着。那咱們徹底能夠先完成後面指令的執行階段。性能
以前我爲你講解的,不管是流水線停頓,仍是操做數前推,歸根到底,只要前面指令的特定階段尚未執行完成,後面的指令就會被「阻塞」住。線程
可是這個「阻塞」不少時候是沒有必要的。由於儘管你的代碼生成的指令是順序的,可是若是後面的指令不須要依賴前面指令的執行結果,徹底能夠沒必要等待前面的指令運算完成。
好比說,下面這三行代碼。3d
計算裏面的 x ,卻要等待 a 和 d 都計算完成,實在沒啥必要。因此咱們徹底能夠在 d 的計算等待 a 的計算的過程當中,先把 x 的結果給算出來。blog
在流水線裏,後面的指令不依賴前面的指令,那就不用等待前面的指令執行,它徹底能夠先執行。排序
能夠看到,由於第三條指令並不依賴於前兩條指令的計算結果,因此在第二條指令等待第一條指令的訪存和寫回階段的時候,第三條指令就已經執行完成了。內存
這就比如你開了一家餐館,顧客會排隊來點菜。餐館的廚房裏會有洗菜、切菜、炒菜、上菜這樣的各個步驟。後廚也是按照點菜的順序開始作菜的。可是不一樣的菜須要花費的時間和工序可能都有差異。有些菜作起來特別麻煩,特別慢。好比作一道佛跳牆有好幾道工序。咱們沒有必要非要等先點的佛跳牆上菜了,再開始作後面的炒雞蛋。只要有廚子空出來了,就能夠先動手作前面的簡單菜,先給客戶端上去。資源
這樣的解決方案,在計算機組成裏面,被稱爲 亂序執行(Out-of-Order Execution,OoOE)。亂序執行,最先來自於著名的IBM 360。相信你必定據說過《人月神話》這本軟件工程屆的經典著做,
它講的就是IBM360開發過程當中的「人生體會」。而IBM 360困難的開發過程,也少不了第一次引入亂序執行這個新的CPU技術。
那麼,咱們的CPU怎樣才能實現亂序執行呢?是否是像玩俄羅斯方塊同樣,把後面的指令,找一個前面的坑填進去就好了?事情並無這麼簡單。其實,從今天軟件開發的維度來思考,亂
序執行好像是在指令的執行階段,引入了一個「線程池」。咱們下面就來看一看,在CPU裏,亂序執行的過程到底是怎樣的。
使用亂序執行技術後,CPU裏的流水線就和我以前給你看的5級流水線不太同樣了。咱們一塊兒來看一看下面這張圖。
1.在取指令和指令譯碼的時候,亂序執行的CPU和其餘使用流水線架構的CPU是同樣的。它會一級一級順序地進行取指令和指令譯碼的工做。
2.在指令譯碼完成以後,就不同了。CPU不會直接進行指令執行,而是進行一次指令分發,把指令發到一個叫做保留站(Reservation Stations)的地方。顧名思義,這個保留站,
就像一個火車站同樣。發送到車站的指令,就像是一列列的火車。
3.這些指令不會馬上執行,而要等待它們所依賴的數據,傳遞給它們以後纔會執行。這就好像一列列的火車都要等到乘客來齊了才能出發。
4.一旦指令依賴的數據來齊了,指令就能夠交到後面的功能單元(Function Unit,FU),其實就是ALU,去執行了。咱們有不少功能單元能夠並行運行,可是不一樣的功能單元可以支持執行的指令並不相同。就和咱們的鐵軌同樣,有些從上海北上,能夠到北京和哈爾濱;有些是南下的,能夠到廣州和深圳。
5.指令執行的階段完成以後,咱們並不能馬上把結果寫回到寄存器裏面去,而是把結果再存放到一個叫做重排序緩衝區(Re-Order Buffer,ROB)的地方。
6.在重排序緩衝區裏,咱們的CPU會按照取指令的順序,對指令的計算結果從新排序。只有排在前面的指令都已經完成了,纔會提交指令,完成整個指令的運算結果。
7.實際的指令的計算結果數據,並非直接寫到內存或者高速緩存裏,而是先寫入存儲緩衝區(StoreBuffer面,最終纔會寫入到高速緩存和內存裏。
能夠看到,在亂序執行的狀況下,只有CPU內部指令的執行層面,多是「亂序」的。只要咱們能在指令的譯碼階段正確地分析出指令之間的數據依賴關係,
這個「亂序」就只會在互相沒有影響的指令之間發生。即使指令的執行過程當中是亂序的,咱們在最終指令的計算結果寫入到寄存器和內存以前,依然會進行一次排序,以確保全部指令在外部看來仍然是有序完成的。
有了亂序執行,咱們從新去執行上面的3行代碼
a = b + c d = a * e x = y * z
裏面的 d 依賴於 a 的計算結果,不會在 a 的計算完成以前執行。可是咱們的CPU並不會閒着,由於 x = y * z的指令一樣會被分發到保留站裏。由於 x 所依賴的 y 和 z 的數據是準備好的,
這裏的乘法運算不會等待計算 d,而會先去計算 x 的值。
若是咱們只有一個FU可以計算乘法,那麼這個FU並不會由於 d 要等待 a 的計算結果,而被閒置,而是會先被拿去計算 x。
在 x 計算完成以後,d 也等來了 a 的計算結果。這個時候,咱們的FU就會去計算出 d 的結果。而後在重排序緩衝區裏,把對應的計算結果的提交順序,仍然設置成 a -> d -> x,
而計算完成的順序是 x -> a -> d。在這整個過程當中,整個計算乘法的FU都沒有閒置,這也意味着咱們的CPU的吞吐率最大化了。
整個亂序執行技術,就好像在指令的執行階段提供一個「線程池」。指令再也不是順序執行的,而是根據池裏所擁有的資源,以及各個任務是否能夠進行執行,進行動態調度。在執行完成以後,又從新把結果在一個隊
列裏面,按照指令的分發順序從新排序。即便內部是「亂序」的,可是在外部看起來,仍然是層次分明地順序執行。
亂序執行,極大地提升了CPU的運行效率。核心緣由是,現代CPU的運行速度比訪問主內存的速度要快不少。若是徹底採用順序執行的方式,不少時間都會浪費在前面指令等待獲取內存數據的時間裏。CPU不得不加入NOP操做進行空轉。而現代CPU的流水線級數也已經相對比較深了,到達了14級。這也意味着,同一個時鐘週期內並行執行的指令數是不少的。
而亂序執行,以及咱們後面要講的高速緩存,彌補了CPU和內存之間的性能差別。一樣,也充分利用了較深的流水行帶來的併發性,使得咱們能夠充分利用CPU的性能。
好了,總結一下。這一講裏,我爲你介紹了亂序執行,這個解決流水線阻塞的技術方案。由於數據的依賴關係和指令前後執行的順序問題,不少時候,流水線不得不「阻塞」在特定的指令上。即便後續別的指令,並不依賴正在執行的指令和阻塞的指令,也不能繼續執行。
而亂序執行,則是在指令執行的階段經過一個相似線程池的保留站,讓系統本身去動態調度先執行哪些指令。這個動態調度巧妙地解決了流水線阻塞的問題。指令執行的前後順序,再也不和它們在程序中的順序有關。咱們只要保證不破壞數據依賴就行了。CPU只要等到在指令結果的最終提交的階段,再經過重排序的方式,確保指令「實際上」是順序執行的。