java開(kāi)發(fā)CPU流水線與指令亂序執(zhí)行詳解
引言
青蛙見(jiàn)了蜈蚣,好奇地問(wèn):"蜈蚣大哥,我很好奇,你那么多條腿,走路的時(shí)候先邁哪一條???"
蜈蚣聽(tīng)后說(shuō):"青蛙老弟,我一直就這么走路,從沒(méi)想過(guò)先邁哪一條腿,等我想一想再回答你。"
蜈蚣站立了幾分鐘,它一邊思考一邊向前,蹣跚了幾步,終于趴下去了。
它對(duì)青蛙說(shuō):“請(qǐng)你再也別問(wèn)其它蜈蚣這個(gè)問(wèn)題了!我一直都在這樣走路,這根本不成問(wèn)題!可現(xiàn)在你問(wèn)我先移動(dòng)哪一條腿,我也不知道了。搞得我現(xiàn)在連路都不會(huì)走了,我該怎么辦呢?”
這個(gè)小故事屬實(shí)反映了我最近的心態(tài):
越學(xué)越不會(huì)了。。。
本來(lái)synchronized和volatile關(guān)鍵字用得好好的,我非要深入研究一下他們的原理,所以研究了內(nèi)存屏障,又研究了和內(nèi)存屏障相關(guān)的MESI,又研究了Cache Coherence和Memory Consistency,發(fā)現(xiàn)一切問(wèn)題都出在CPU身上。于是又驚嘆Java一次編寫到處運(yùn)行的特性,最終又研究到JMM。
說(shuō)是研究,其實(shí)就是把學(xué)習(xí)過(guò)程中自己拋出來(lái)的問(wèn)題解決掉,把所有知識(shí)穿成一條線罷了。
這條線的線頭就從指令的亂序執(zhí)行開(kāi)始了。
經(jīng)典的指令亂序執(zhí)行的原因有兩種,分別是Compiler Reordering和CPU Reordering。
1. Compiler Reordering
編譯器會(huì)對(duì)高級(jí)語(yǔ)言的代碼進(jìn)行分析,如果它認(rèn)為你的代碼可以優(yōu)化,那么他會(huì)對(duì)你的代碼進(jìn)行各種優(yōu)化然后生成匯編指令。當(dāng)然,本文說(shuō)的優(yōu)化主要是指令重排(Compiler Reordering)。
但是編譯器的優(yōu)化必須滿足特定的條件,一個(gè)非常重要的原則就是as-if-serial語(yǔ)義:
Allows any and all code transformations that do not change the observable behavior of the program.
編譯器必須遵守as-if-serial語(yǔ)義,也就是編譯器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。 但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。
我們用非常簡(jiǎn)單的C++代碼舉個(gè)例子(因?yàn)榫幾g更簡(jiǎn)單,看起來(lái)也更直觀)。
int a,b,c;
void bar()
{
a = c + 1;
b = 1;
}
int main()
{
bar();
return 0;
}
我們對(duì)這段代碼進(jìn)行變異,讓編譯器在O2級(jí)別優(yōu)化的情況下編譯代碼,我截取其中的bar()的匯編代碼,如下所示:
_Z3barv:
.LFB0:
.cfi_startproc
endbr64
movl $1, b(%rip) #將1的值賦給b,即b = 1
movl c(%rip), %eax #將c的值放到寄存器%eax中
addl $1, %eax #將寄存器%eax的值+1,即c + 1
movl %eax, a(%rip) #將寄存器%eax的值賦給a,即a = c + 1
ret
我們發(fā)現(xiàn),編譯得到的匯編代碼和我們?cè)镜腃語(yǔ)言代碼順序并不一致。
匯編指令先執(zhí)行了b = 1,之后才執(zhí)行了a = c + 1。說(shuō)明變量a和b的store操作并沒(méi)有按照他們?cè)诔绦蛑卸x的順序來(lái)執(zhí)行。
既然匯編指令被重排了,CPU的執(zhí)行順序自然是根據(jù)匯編指令對(duì)應(yīng)的機(jī)器指令執(zhí)行的,大概率也會(huì)被重排。其實(shí)除此之外,CPU本身也會(huì)對(duì)指令進(jìn)行重排(CPU Reordering)。
2. CPU 流水線
談及處理器必談及流水線,處理器的流水線結(jié)構(gòu)是處理器微架構(gòu)最基本的一個(gè)要素,也是造成CPU Reordering的主要因素。
2.1. 從汽車裝配談起
流水線的概念始于工業(yè)制造領(lǐng)域,但是鑒于大部分人其實(shí)都沒(méi)接觸過(guò)流水線,我們不妨舉一個(gè)汽車生產(chǎn)的例子來(lái)解釋流水線的誕生。
我們首先粗淺地認(rèn)為汽車的裝配需要兩個(gè)步驟:
- 制作零件:制作車身外殼、發(fā)動(dòng)機(jī)和各種其他部件;
- 組裝:將各零部件(自己制作和外采的所有零部件)組裝成車。

假設(shè)一個(gè)工人進(jìn)行每個(gè)步驟都占用1個(gè)月,如果不采用流水線,而采用串行方式來(lái)執(zhí)行的話,一年時(shí)間可以裝配6輛汽車,過(guò)程見(jiàn)下圖:

串行的效率實(shí)在是太有限了,根本原因就是裝配的兩個(gè)步驟都是由一個(gè)人完成的。如果有人能在組裝進(jìn)行的同時(shí)制作零件,效率會(huì)大大提升,也就是每個(gè)流程只專注一件事情,我們?cè)僖胍粋€(gè)工人。

這樣一個(gè)人專門負(fù)責(zé)制作零件,另一個(gè)人專門組裝零件,兩個(gè)工作交疊進(jìn)行,過(guò)程見(jiàn)下圖:

增加一個(gè)人手之后,除了第一個(gè)月,每一個(gè)月都有完整的制作零件和組裝流程,因此一年內(nèi)可以完成11臺(tái)汽車的裝配(相比于串行方式的6臺(tái),幾乎翻倍了),從第二年開(kāi)始,每年就能裝配12臺(tái)了(直接翻倍)。
這個(gè)過(guò)程就是流水線的執(zhí)行過(guò)程,因?yàn)槲覀儼哑嚨闹谱鬟^(guò)程分成了兩個(gè)步驟,因此以上流水線成為二級(jí)流水線。
我們繼續(xù)優(yōu)化,我們將制作零件的步驟分成時(shí)間周期更短的沖壓和焊接兩步,將組裝步驟分為時(shí)間周期更短的涂裝和總裝兩步,并且假設(shè)每個(gè)步驟的時(shí)間周期為0.5個(gè)月。
當(dāng)然嘍,我們得再雇傭倆人。

現(xiàn)在就是四級(jí)流水線了,神奇的事情發(fā)生了,四級(jí)流水線使得原本需要一年時(shí)間的任務(wù)現(xiàn)在只需要4.5個(gè)月便可以完成,再次提升了效率。如下圖所示:

2.2. 現(xiàn)代CPU的流水線
現(xiàn)代 CPU 支持多級(jí)指令流水線,例如支持同時(shí)執(zhí)行 取指令 - 指令譯碼 - 執(zhí)行指令 - 內(nèi)存訪問(wèn) - 數(shù)據(jù)寫回的處理器,就可以稱之為五級(jí)指令流水線。
這時(shí) CPU 可以在一個(gè)時(shí)鐘周期內(nèi),同時(shí)運(yùn)行五條指令的不同階段,其中每個(gè)階段的都占用一個(gè)或多個(gè)指令周期(CPU以執(zhí)行時(shí)間最長(zhǎng)),本質(zhì)上,流水線技術(shù)井不能縮短單條指令的執(zhí)行時(shí)間,但它變相地提高了指令的吞吐率。

上面的CPU流水線圖并非特定型號(hào)的CPU的示例,而是為了說(shuō)明幾個(gè)問(wèn)題特意畫成了這個(gè)樣子。
- 通常而言,CPU設(shè)計(jì)者會(huì)選擇執(zhí)行時(shí)間最長(zhǎng)的流水線階段作為一個(gè)時(shí)鐘周期,這樣能保證其他階段能在一個(gè)時(shí)鐘周期內(nèi)完成,避免出現(xiàn)流水線斷流。
- 每一個(gè)流水線級(jí)的時(shí)間都是一個(gè)時(shí)鐘周期,但是其中實(shí)際操作的時(shí)間,可能短于一個(gè)時(shí)鐘周期。比如譯碼器其實(shí)就是一個(gè)組合邏輯電路,門延遲很低,就不需要一個(gè)完整的時(shí)鐘周期就能完成自己的任務(wù),任務(wù)完成之后CPU其實(shí)是在“等待”。
很多人可能會(huì)問(wèn),既然流水線這么好用,那為什么CPU設(shè)計(jì)者不設(shè)計(jì)一個(gè)超長(zhǎng)流水線呢?這就需要說(shuō)明一下超長(zhǎng)流水線的瓶頸了。
3. 超長(zhǎng)流水線的瓶頸
3.1. 性能瓶頸
流水線長(zhǎng)度的增加,是有性能成本的。
每一級(jí)流水線的輸出都需要放在流水線寄存器中,然后再下一個(gè)時(shí)鐘周期,交給下一個(gè)流水線級(jí)去處理。每增加一級(jí)流水線,就要多一級(jí)寫入流水線寄存器的操作。
以多線程為例,數(shù)量合適的多線程會(huì)提高數(shù)據(jù)的處理速度,但是當(dāng)線程數(shù)量太多,線程之間的時(shí)間切換成本就無(wú)法被忽視,線程的增加甚至可能成為性能提升的負(fù)擔(dān)。
3.2. 功耗瓶頸
提升流水線的深度,需要同步提高CPU的主頻。再看一下這個(gè)圖:

由于流水線的每一級(jí)被分得特別細(xì),甚至有的還沒(méi)有完全占滿單個(gè)時(shí)鐘周期,也就意味著單個(gè)時(shí)鐘周期內(nèi)能完成的事情變少了,因此只有提升主頻,CPU 在指令的響應(yīng)時(shí)間這個(gè)指標(biāo)上才能保持和原來(lái)相同的性能。
提升主頻和流水線深度就以為這晶體管的增加,也就以為這功耗變大。
沒(méi)人想擁有一臺(tái)“充電3小時(shí),辦公20分鐘”的一臺(tái)筆記本電腦吧。
3.3. 指令亂序
還是以上面的圖為例(就不再貼一遍了),指令1的訪存操作使用了多個(gè)時(shí)鐘周期,導(dǎo)致指令2和指令3在指令1之前完成了。
如果是一般的代碼還好,但如果是具有依賴性的代碼,比如:
float a = 3.14159 * 0.2; // 指令1 float b = a * 2; // 指令2 float c = b + 1; // 指令3 float d = 10; // 指令4
指令1、2、3的執(zhí)行順序就絕不能向圖中表示的那樣亂序執(zhí)行。其中有兩點(diǎn)需要我們注意:
- 由于上圖中情形的存在,導(dǎo)致CPU確實(shí)有可能出現(xiàn)亂序執(zhí)行的情況;
- CPU需要阻止具有依賴關(guān)系的指令亂序執(zhí)行(指令1,2,3),轉(zhuǎn)而讓后續(xù)沒(méi)有依賴關(guān)系的指令(指令4)先執(zhí)行。
對(duì)于第2條,如果流水線只有5級(jí)還好說(shuō),CPU自然有辦法判斷哪些指令具有依賴性,并拒絕做出指令亂序。但是如果有20條流水線,CPU肯定還有辦法判斷,但是可想而知,這種判斷勢(shì)必會(huì)影響CPU的性能。
回到本文一開(kāi)始說(shuō)的編譯器指令重排序,當(dāng)然嘍,也包含Java的JIT將字節(jié)碼編譯成機(jī)器碼時(shí)的指令重排序,就是為了把沒(méi)有依賴關(guān)系的指令放一起,本質(zhì)上都是為了適配CPU,更好地發(fā)揮出CPU流水線的功能,從而提升性能罷了。
4. 總結(jié)
說(shuō)了這么多,很可能在我之后的文章中被一句話帶過(guò)。
其實(shí)我想表達(dá)的思想就是,實(shí)際代碼運(yùn)行的順序可能和我們代碼編寫的順序并不一致。記住這句話很容易,但或許總會(huì)有人像我一樣想稍微深入一點(diǎn)來(lái)了解這句話的本質(zhì)吧。
除了本文所述,CPU和高速緩存之間的交互過(guò)程中,硬件工程師也著實(shí)給軟件開(kāi)發(fā)者挖了不少坑,內(nèi)存屏障就是在這種背景下產(chǎn)生的。
以上就是java開(kāi)發(fā)CPU流水線與指令亂序執(zhí)行詳解的詳細(xì)內(nèi)容,更多關(guān)于java CPU流水線指令亂序執(zhí)行的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java設(shè)計(jì)模式模板方法(Template)原理解析
這篇文章主要介紹了Java設(shè)計(jì)模式模板方法(Template)原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11
java 實(shí)現(xiàn)最小二叉樹(shù)堆排序的實(shí)例
這篇文章主要介紹了java 實(shí)現(xiàn)最小二叉樹(shù)堆排序的實(shí)例的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-09-09
教你如何使用Java實(shí)現(xiàn)WebSocket
這篇文章主要介紹了教你如何使用Java實(shí)現(xiàn)WebSocket問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
Java?中導(dǎo)入excel時(shí)使用?trim()?無(wú)法去除空格的問(wèn)題解決方案
這篇文章主要介紹了Java中導(dǎo)入excel時(shí)使用trim()無(wú)法去除空格的解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06
Tomcat 服務(wù)器 在45秒內(nèi)未啟動(dòng)成功的解決方法
下面小編就為大家?guī)?lái)一篇Tomcat 服務(wù)器 在45秒內(nèi)未啟動(dòng)成功的解決方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
Spring Boot中使用jdbctemplate 操作MYSQL數(shù)據(jù)庫(kù)實(shí)例
本篇文章主要介紹了Spring Boot中使用jdbctemplate 操作MYSQL數(shù)據(jù)庫(kù)實(shí)例,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-04-04
JUC中的wait與notify方法實(shí)現(xiàn)原理詳解
這篇文章主要介紹了JUC中的wait與notify方法實(shí)現(xiàn)原理,在進(jìn)行wait()之前,就代表著需要爭(zhēng)奪Synchorized,而Synchronized代碼塊通過(guò)javap生成的字節(jié)碼中包含monitor?enter和monitor?exit兩個(gè)指令2023-03-03
Mybatis使用JSONObject接收數(shù)據(jù)庫(kù)查詢的方法
這篇文章主要介紹了Mybatis使用JSONObject接收數(shù)據(jù)庫(kù)查詢,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12

