不懂精簡指令集還敢說自己是程序員?
在上一篇文章《CPU進(jìn)化論:復(fù)雜指令集》中我們從歷史的角度講述了復(fù)雜指令集出現(xiàn)的必然,隨著時間的推移,采用復(fù)雜指令集架構(gòu)的CPU出現(xiàn)各種各樣的問題,面對這些問題一部分人開始重新思考指令集到底該如何設(shè)計。
在這一時期,兩個趨勢的出現(xiàn)促成一種新的指令集設(shè)計思想。
內(nèi)存與編譯器
時間來到了1980s年代,此時容量“高達(dá)”64K的內(nèi)存開始出現(xiàn),內(nèi)存容量上終于不再捉襟見肘,價格也開始急速下降,在1977年,1MB內(nèi)存的價格高達(dá)$5000,要知道這可是1977年的5000刀,但到了1994年,1MB內(nèi)存價格就急速下降到大概只有$6,這是第一個趨勢。
此外在這一時期隨著編譯技術(shù)的進(jìn)步,編譯器越來越成熟,漸漸的程序員們開始依靠編譯器來生成匯編指令而不再自己手工編寫。
這兩個趨勢的出現(xiàn)讓人們有了更多思考。
化繁為簡
19世紀(jì)末20世紀(jì)初意大利經(jīng)濟(jì)學(xué)家Pareto發(fā)現(xiàn),在任何一組東西中,最重要的只占其中一小部分,約20%,其余80%盡管是多數(shù),卻是次要的,這就是著名的二八定律,機(jī)器指令的執(zhí)行頻率也有類似的規(guī)律。
大概80%的時間CPU都在執(zhí)行那20%的機(jī)器指令,同時CISC中一部分比較復(fù)雜的指令并不怎么被經(jīng)常用到,而且那些設(shè)計編譯器的程序員也更傾向于組合一些簡單的指令來完成特定任務(wù)。
與此同時我們在上文提到過的一位計算機(jī)科學(xué)家,被派去改善微代碼設(shè)計,但后來這老哥發(fā)現(xiàn)有問題的是微代碼本身,因此開始轉(zhuǎn)過頭來去思考微代碼這種設(shè)計的問題在哪里。
他的早期工作提出一個關(guān)鍵點,復(fù)雜指令集中那些被認(rèn)為可以提高性能的指令其實在內(nèi)部被微代碼拖后腿了,如果移除掉微代碼,程序反而可以運行的更快,并且可以節(jié)省構(gòu)造CPU消耗的晶體管數(shù)量。
由于微代碼的設(shè)計思想是將復(fù)雜機(jī)器指令在CPU內(nèi)部轉(zhuǎn)為相對簡單的機(jī)器指令,這一過程對編譯器不可見,也就是說你沒有辦法通過編譯器去影響CPU內(nèi)部的微代碼運行行為,因此如果微代碼出現(xiàn)bug那么編譯器是無能為力的,你沒有辦法通過編譯器生成其它機(jī)器指令來修復(fù)問題而只能去修改微代碼本身。
此外他還發(fā)現(xiàn),有時一些復(fù)雜的機(jī)器指令執(zhí)行起來要比等價的多個簡單指令要。
這一切都在提示:為什么不直接用一些簡單到指令來替換掉那些復(fù)雜的指令呢?
精簡指令集哲學(xué)
基于對復(fù)雜指令集的思考,精簡指令集哲學(xué)誕生了,精簡指令集主要體現(xiàn)在以下三個方面:
1,指令本身的復(fù)雜度
精簡指令集的思想其實很簡單,干嘛要去死磕復(fù)雜的指令,去掉復(fù)雜指令代之以一些簡單的指令。
有了簡單指令CPU內(nèi)部的微代碼也不需要了,沒有了微代碼這層中間抽象,編譯器生成的機(jī)器指令對CPU的控制力大大增強,有什么問題讓寫編譯器的那幫家伙修復(fù)就好了,顯然調(diào)試編譯器這種軟件要比調(diào)試CPU這種硬件要簡單很多。
注意,精簡指令集思想不是說指令集中指令的數(shù)量變少,而是說一條指令背后代表的動作更簡單了。
舉個簡單的例子,復(fù)雜指令集中的一條指令背后代表的含義是“吃飯”的全部過程,而精簡指令集中的一條指令僅僅表示“咀嚼一下”的其中一個小步驟。
博主在《你管這破玩意叫編程語言》一文中舉得例子其實更形象一些,復(fù)雜指令集下一條指令可以表示“給我端杯水”,而在精簡指令集下你需要這樣表示:
2,編譯器
精簡指令集的另一個特點就是編譯器對CPU的控制力更強。
在復(fù)雜指令集下,CPU會對編譯器隱藏機(jī)器指令的執(zhí)行細(xì)節(jié),就像微代碼一樣,編譯器對此無能為力。
而在精簡指令集下CPU內(nèi)部的操作細(xì)節(jié)暴露給編譯器,編譯器可以對其進(jìn)行控制,也因此,精簡指令集RISC還有一個有趣的稱呼:“Relegate Interesting Stuff to Compiler”,把一些有趣的玩意兒讓編譯器來完成。
3,load/store architecture
在復(fù)雜指令集下,一條機(jī)器指令可能涉及到從內(nèi)存中取出數(shù)據(jù)、執(zhí)行一些操作比如加和、然后再把執(zhí)行結(jié)果寫回到內(nèi)存中,注意這是在一條機(jī)器指令下完成的。
但在精簡指令集下,這絕對是大寫的禁忌,精簡指令集下的指令只能操作寄存器中的數(shù)據(jù),不可以直接操作內(nèi)存中的數(shù)據(jù),也就是說這些指令比如加法指令不會去訪問內(nèi)存。
畢竟數(shù)據(jù)還是存放在內(nèi)存中的,那么誰來讀寫內(nèi)存呢?
原來在精簡指令集下有專用的 load 和 store 兩條機(jī)器指令來負(fù)責(zé)內(nèi)存的讀寫,其它指令只能操作CPU內(nèi)部的寄存器,這是和復(fù)雜指令集一個很鮮明的區(qū)別。
你可能會好奇,用兩條專用的指令來讀寫內(nèi)存有什么好處嗎?別著急,在本文后半部分我們還會回到load/store指令。
以上就是三點就是精簡指令集的設(shè)計哲學(xué)。
接下來我們用一個例子來看下RISC和CISC的區(qū)別。
兩數(shù)相乘
如圖所示就是最經(jīng)典的計算模型,最右邊是內(nèi)存,存放機(jī)器指令和數(shù)據(jù),最左側(cè)是CPU,CPU內(nèi)部是寄存器和計算單元ALU,進(jìn)一步了解CPU請參考《你管這破玩意叫CPU?》
內(nèi)存中的地址A和地址B分別存放了兩個數(shù),假設(shè)我們想計算這兩個數(shù)字之和,然后再把計算結(jié)果寫回內(nèi)存地址A。
我們分別來看下在CISC和在RISC下的會怎樣實現(xiàn)。
1,CISC
復(fù)雜指令集的一個主要目的就是讓盡可能少的機(jī)器指令來完成盡可能多的任務(wù),在這種思想下CPU需要在從內(nèi)存中拿到一條機(jī)器指令后“自己去完成一系列的操作”,這部分操作對外不可見。
在這種方法下,CISC中可能會存在一條叫做MULT的機(jī)器指令,MULT是乘法multiplication的簡寫。
當(dāng)CPU執(zhí)行MULT這條機(jī)器指令時需要:
從內(nèi)存中加載地址A上的數(shù),存放在寄存器中
從內(nèi)存中夾雜地址B上的數(shù),存放在寄存器中
ALU根據(jù)寄存器中的值進(jìn)行乘積
將乘積寫回內(nèi)存
以上這幾部統(tǒng)統(tǒng)都可以用這樣一條指令來完成:
- MULT A B
MULT就是所謂的復(fù)雜指令了,從這里我們也可以看出,復(fù)雜指令并不是說“MULT A B”這一行指令本身有多復(fù)雜,而是其背后所代表的任務(wù)復(fù)雜。
這條機(jī)器指令直接從內(nèi)存中加載數(shù)據(jù),程序員(寫匯編語言或者寫編譯器的程序員)根本就不要自己顯示的從內(nèi)存中加載數(shù)據(jù),實際上這條機(jī)器指令已經(jīng)非常類似高級語言了,我們假設(shè)內(nèi)存地址A中的值為變量a,地址B中的值為變量b,那么這條機(jī)器指令基本等價于高級語言中這樣一句:
- a = a * b;
這就是我們在上一篇《CPU進(jìn)化論:復(fù)雜指令集》中提到的所謂抹平差異,semantic gap,抹平高級語言和機(jī)器指令之間的差異,讓程序員或者編譯器使用最少的代碼就能完成任務(wù),因為這會節(jié)省程序本身占用的內(nèi)存空間,要知道在在1977年,1MB內(nèi)存的價格大概需要$5000,省下來的就是錢。
因為一條機(jī)器指令背后的操作很多,而程序員僅僅就寫了一行“MULT A B”,這行指令背后的復(fù)雜操作就必須由CPU直接通過硬件來實現(xiàn),這加重了CPU 硬件本身的復(fù)雜度,需要的晶體管數(shù)量也更多。
接下來我們看RISC方法。
2,RISC
相比之下RISC更傾向于使用一系列簡單的指令來完成一項任務(wù),我們來看下一條MULT指令需要完成的操作:
從內(nèi)存中加載地址A上的數(shù),存放在寄存器中
從內(nèi)存中夾雜地址B上的數(shù),存放在寄存器中
ALU根據(jù)寄存器中的值進(jìn)行乘積
將乘積寫回內(nèi)存
這幾步需要a)從內(nèi)存中讀數(shù)據(jù);b)乘積;c) 向內(nèi)存中寫數(shù)據(jù),因此在RISC下會有對應(yīng)的LOAD、PROD、STORE指令來分別完成這幾個操作。
Load指令會將數(shù)據(jù)從內(nèi)存搬到寄存器;PROD指令會計算兩個寄存器中數(shù)字的乘積;Store指令把寄存器中的數(shù)據(jù)寫回內(nèi)存,因此如果一個程序員想完成上述任務(wù)就需要寫這些匯編指令:
- LOAD RA, A
- LOAD RB, B
- PROD RA, RB
- STORE A, RA
現(xiàn)在你應(yīng)該看到了,同樣一項任務(wù),在CISC下只需要一條機(jī)器指令,而在RISC下需要四條機(jī)器指令,顯然RISC下的程序本身所占據(jù)的空間要比CISC大,而且這對直接用匯編語言來寫程序的程序員來說是很不友好的,因為更繁瑣嘛!再來看看這樣圖感受一下:
但RISC設(shè)計的初衷也不是讓程序員直接使用匯編語言來寫程序,而是把這項任務(wù)交給編譯器,讓編譯器來生成機(jī)器指令。
標(biāo)準(zhǔn)從來都是一個好東西
讓我們再來仔細(xì)的看一下RISC下生成的幾條指令:
- LOAD RA, A
- LOAD RB, B
- PROD RA, RB
- STORE A, RA
這些指令都非常簡單,CPU內(nèi)部不需要復(fù)雜的硬件邏輯來進(jìn)行解碼,因此更節(jié)省晶體管,這些節(jié)省下來的晶體管可用于其它功能上。
最關(guān)鍵的是,注意,由于每一條指令都很簡單,執(zhí)行的時間都差不多,因此這使得一種能高效處理機(jī)器指令的方法成為可能,這項技術(shù)是什么呢?
我們在《CPU遇上特斯拉,程序員的心思你別猜》這篇文章中提到過,這就是有名的流水線技術(shù)。
指令流水線
流水線技術(shù)是初期精簡指令集的殺手锏。
在這里我們還是以生產(chǎn)汽車(新能源)為例來介紹一下。
假設(shè)組裝一輛汽車需要經(jīng)過四個步驟:組裝車架、安裝引擎、安裝電池、檢驗。
假設(shè)這每個步驟需要10分鐘,如果沒有流水線技術(shù),那么生產(chǎn)一輛汽車的時間是40分鐘,只有第一輛汽車完整的經(jīng)過這四個步驟后下一輛車才能進(jìn)入生產(chǎn)車間。
這就是最初復(fù)雜指令集CPU的工作場景。
顯然這是相當(dāng)?shù)托У模驗楫?dāng)前一輛車在進(jìn)行最后一個步驟時,前三個步驟:組裝車架、安裝引擎、安裝電池,這三個步驟的工人是空閑。
CPU的道理也是一樣的,低效的原因在于沒有充分利用資源,在這種方法下有人會偷懶。
但引入流水線技術(shù)就不一樣了,當(dāng)?shù)谝惠v車還在安裝引擎時后一輛車就可以進(jìn)入流水線來組裝車架了,采用流水線技術(shù),四個步驟可以同時進(jìn)行,最大可能的充分利用資源。
原來40分鐘才能生產(chǎn)一輛車,現(xiàn)在有了流水線技術(shù)可以10分鐘就生產(chǎn)出一輛車。
注意,這里的假設(shè)是每個步驟都需要10分鐘,如果流水線每個階段的耗時不同,將顯著影響流水線的處理能力。
假如其中一個步驟,安裝電池,需要20分鐘,那么安裝電池的前一個和后一個步驟就會有10分鐘的空閑,這顯然不能充分利用資源。
精簡指令集的設(shè)計者們當(dāng)然也明白這個道理,因此他們嘗試讓每條指令執(zhí)行的時間都差不多一樣,盡可能讓流水線更高效的處理機(jī)器指令,而這也是為什么在精簡指令集中存在Load和Store兩條訪問內(nèi)存指令的原因。
由于復(fù)雜指令集指令與指令之間差異較大,執(zhí)行時間參差不齊,沒辦法很好的以流水線的方式高效處理機(jī)器指令(后續(xù)我們會看到復(fù)雜指令集會改善這一點)。
第一代RISC處理器即為全流水線設(shè)計,典型的就是五級流水線,大概1到2個時鐘周期就能執(zhí)行一條指令,而這一時期的CISC大概5到10個時鐘周期才能執(zhí)行一條指令,盡管RISC架構(gòu)下編譯出的程序需要更多指令,但RISC精簡的設(shè)計使得RISC架構(gòu)下的CPU更緊湊,消耗更少的晶體管(無需微代碼),因此帶來更高的主頻,這使得RISC架構(gòu)下的CPU完成相同的任務(wù)速度優(yōu)于CISC。
有流水線技術(shù)的加持,采用精簡指令集設(shè)計的CPU在性能上開始橫掃其復(fù)雜指令集對手。
名揚天下
到了1980年代中期,采用精簡指令集的商業(yè)CPU開始出現(xiàn),到1980年代后期,采用精簡指令集設(shè)計的CPU就在性能上輕松碾壓所有傳統(tǒng)設(shè)計。
到了1987年采用RISC設(shè)計的MIPS R2000處理器在性能上是采用CISC架構(gòu)(x86)的Intel i386DX兩到三倍。
所有其它CPU生成廠商都開始跟進(jìn)RISC,積極采納精簡指令集設(shè)計思想,甚至操作系統(tǒng)MINIX(就是那個Linus上大學(xué)時使用的操作系統(tǒng))的作者Andrew Tanenbaum在90年代初預(yù)言:“5年后x86將無人問津”,x86正是基于CISC。
CISC迎來至暗時刻。
接下來CISC該如何絕地反擊,要知道Inter以及AMD (x86處理器兩大知名生產(chǎn)商) 的硬件工程師們絕非等閑之輩。
預(yù)知后事如何,請聽下回分解。
總結(jié)
CISC中微代碼設(shè)計的復(fù)雜性讓人們重新思考CPU到底該如何設(shè)計,基于對執(zhí)行指令的重新審視RISC設(shè)計哲學(xué)應(yīng)運而生。
RISC中每條指令更加簡單,執(zhí)行時間比較標(biāo)準(zhǔn),因此可以很高效的利用流水線技術(shù),這一切都讓采用RISC架構(gòu)的CPU獲得了很好性能。
面對RISC,CISC陣營也開始全面反思應(yīng)如何應(yīng)對挑戰(zhàn)。后續(xù)文章將繼續(xù)這一話題。
希望本文對大家理解精簡指令集有所幫助。
本文轉(zhuǎn)載自微信公眾號「碼農(nóng)的荒島求生」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系碼農(nóng)的荒島求生公眾號。