計算機的基本硬件組成
早年,要自己組裝一臺計算機,要先有三大件,CPU、內存和主板。
在這三大件中,我們首先要說的是CPU,它是計算機最重要的核心配件,全名叫中央處理器(Central Processing Unit)。為什么說CPU是"最重要"的呢?因為計算機的所有"計算"都是由CPU來進行的。自然,CPU也是整臺計算機中造價最昂貴的部份之一。

第二個重要的配件,就是內存(Memory)。你撰寫的程序、打開的瀏覽器、運行的游戲,都要加載到內存里才能運行。程序讀取的數(shù)據、計算得到的結果,也都要放在內存里。內存越大, 能加在的東西自然也就越多。

存放在內存里的程序和數(shù)據,需要被CPU讀取,CPU計算完之后,還要把數(shù)據寫回到內存。然而CPU不能直接插到內存上,反之亦然。于是,就帶來了最后一個大件----主板(Motherboard)。
主板是一個有著各種各樣,有時候多達數(shù)十乃至上百個插槽的配件。我們的CPU要插在主板上,內存也要插在主板上。主板的芯片組(Chipset)和總線(Bus)解決了CPU和內存之間如何通信的問題。芯片組控制了數(shù)據傳輸?shù)牧鬓D,也就是數(shù)據從哪里到哪里的問題??偩€則是實際數(shù)據傳輸?shù)母咚俟?。因此?b>總線速度(Bus Speed)決定了數(shù)據能傳輸多快。

有了三大件,只要配上電源供電,計算機差不多就可以跑起來了。但是現(xiàn)在還缺少各類輸入(Input)/輸出(Output)設備,也就是我們常說的I/O設備。如果你用的是自己的個人電腦,那顯示器肯定必不可少,只有有了顯示器我們才能看到計算機輸出的各種圖像、文字,這也就是所謂的輸出設備。
同樣的,鼠標和鍵盤也都是必不可少的配件。
最后,你自己配的個人計算機,還要配上一個硬盤。這樣各種數(shù)據才能持久地保存下來。絕大部分人都會給自己的機器裝上一個機箱,配上風扇,解決灰塵和散熱的問題。不過機箱和風扇,算不上計算機的必備硬件。
還有一個很特殊的設備,就是顯卡(Graphics Card)?,F(xiàn)在,使用圖形界面操作系統(tǒng)的計算機,無論是Windows、Mac OS還是Linux,顯卡都是必不可少的。有人可能要說了,我裝機的時候沒有買顯卡,計算機一樣可以正常跑起來??!那是因為,現(xiàn)在的主板都帶了內置的顯卡。如果你用計算機玩游戲,做圖形渲染或者跑深度學習應用,你多半就需要買一張單獨的顯卡,插在主板上。顯卡之所以特殊,是因為顯卡里有除了CPU之外的另一個"處理器",也就是GPU(Graphics Processing Unit,圖形處理器),GPU一樣可以做各種"計算"的工作。
鼠標、鍵盤以及硬盤,這些都插在主板上的。作為外部I/O設備,它們通過主板上的南橋(SouthBridge)芯片組,來控制和CPU之間的通信。"南橋"芯片的名字很直觀,一方面,它在主板上的位置,通常在主板的"南面"。另一方面,它的作用就是作為"橋",來連接鼠標、鍵盤以及硬盤這些外部設備和CPU之間的通信。
有了南橋,自然對應著也有"北橋"。是的,以前的主板上通常也有"北橋"芯片,用來作為"橋",連接CPU和內存、顯卡之間的通信。不過,隨著時間的變遷,現(xiàn)在的主板上的"北橋"芯片的工作,已經被遷移到了CPU的內部,所以你在主板上,已經看不到北橋芯片了。
馮·諾伊曼體系結構
剛才我們講了一臺計算機的硬件組成,這說的是我們平時用的個人電腦或者服務器。那我們平時最常用的智能手機的組成,也是這樣嗎?
我們手機里只有SD卡(Secure Digiral Memory Card)這樣類似硬盤功能的存儲卡插槽,并沒有內存插槽、CPU插槽這些東西。沒錯,因為手機尺寸的原因,手機制造商們選擇把CPU、內存、網絡通信,乃至攝像頭芯片,都封裝到一個芯片,然后再嵌入到手機主板上。這種方式叫SOC,也叫System on a Chip(系統(tǒng)芯片)。
這樣看起來,個人電腦和智能手機的硬件組成方式不太一樣??墒牵覀儗懼悄苁謾C上的App,和寫個人電腦的客戶端應用似乎沒有什么差別,都是通過"高級語言"這樣的編程語言撰寫、編譯之后,一樣是把代碼和數(shù)據加載到內存里來執(zhí)行。這是為什么呢?因為,無論是個人電腦、服務器、智能手機,還是Raspberry Pi這樣的微型卡片機,都遵循著同一個"計算機"的抽象概念。這是怎么樣一個"計算機"呢?這其實就是,計算機祖師爺之一馮·諾伊曼體系結構(Von Neumann architecture),也叫存儲程序計算機。
什么是存儲程序計算機呢?這里面其實暗含了兩個概念,一個是"可編程"計算機,一個是"存儲"計算機。
說到"可編程",估計你會有點懵,你可以先想想,什么是"不可編程"。計算機是由各種門電路組合而成的,然后通過組裝出一個固定的電路板,來完成一個特定的計算程序。一旦需要修改功能,就要重新組裝電路。這樣的話,計算機就是"不可編程"的,因為程序在計算機硬件層面是"寫死"的。最常見的就是老式計算器,電路板設計好了加減乘除,做不了任何計算邏輯固定之外的事情。

我們再來看"存儲"計算機。這其實是說,程序本身是存儲在計算機內存里,可以通過加載不同的程序來解決不同的問題。有"存儲程序計算機",自然也有不能存儲程序的計算機。典型的就是早年的"Plugboard"這樣的插線板式的計算機。整個計算機就是一個巨大的插線板,通過在板子上不同的插頭后者接口的位置插入線路,來實現(xiàn)不同的功能。這樣的計算機自然是"可編程"的,但是編寫好的程序不能存儲下來供下一個加載使用,不得不每次要用到和當前不同的"程序"的時候,重新插板子,重新"編程"。

可以看到,無論是"不可編程"還是"不可存儲",都會讓使用計算機的效率大大下降。而這個對于效率的追求,也就是"存儲程序計算機"的由來。
于是我們的祖師爺,基于當是在秘密開發(fā)的EDVAC寫了一篇報告First Draft of a Report on the EDVAC,描述了他心目中的一臺計算機應該長什么樣。這篇報告在歷史上有個很特殊的簡稱,叫First Draft,翻譯成中文,其實就是《第一份草案》。這樣,現(xiàn)代計算機的發(fā)展就從祖師爺寫的一份草案開始了。
First Fraft里面說了一臺計算機應該有哪些組成,我們一起來看看。
首先是一個包含算術邏輯單元(Arithmetic Logic Unit,ALU)和處理器寄存器(Processor Register)的處理器單元(Processing Unit),用來完成各種算術和邏輯運算。因為它能夠完成各種數(shù)據的處理或者計算工作,因此也有人把這個叫做數(shù)據通路(Datapath)或者運算器。
然后是一個包含指令寄存器(Instruction Register)和 程序計數(shù)器(Program Counter)的控制器單元(Control Unit, CU),用來控制程序的流程,通常就是不同條件下的分支和跳轉。在現(xiàn)在的計算機里,上面的算術邏輯單元和這里的控制器單元,共同組成了我們說的CPU。
接著是用來存儲數(shù)據(Data)和指令(Instruction)的內存。以及更大容量的外部存儲,在過去,可能是磁帶、磁鼓這樣的設備,現(xiàn)在通常就是硬盤。
最后就是各種輸入和輸出設備,以及對應的輸入輸出機制。我們現(xiàn)在無論是使用什么樣的計算機,其實都是和輸入輸出設備在打交道。個人電腦的鼠標鍵盤是輸入設備,顯示器是輸出設備。我們用的智能手機,觸摸屏即是輸入設備,又是輸出設備。而跑在各種云上的服務器,則是通過網絡來進行輸入和輸出。這個時候,網卡即是輸入設備又是輸出設備。
任何一臺計算機的任何一個部件都可以歸到運算器、控制器、存儲器、輸入設備和輸出設備中,而所有現(xiàn)代計算機也是基于這個基礎架構來設計開發(fā)的。
而所有的計算機程序,也都可以抽象為從輸入設備讀取輸入信息,通過運算器和控制器來執(zhí)行存儲在存儲器里的程序,最終把結果輸出到輸出設備中。而我們所有撰寫的無論高級還是低級語言,也都是基于這樣一個抽象框架來進行運行的。

總結延伸
可以說,馮·諾伊曼體系結構確立了我們現(xiàn)在使用的計算機的基礎架構。因此,學習計算機組成原理,其實就是學習和拆解馮·諾伊曼體系結構。
具體來說,學習組成原理,其實就是學習控制器、運算器的工作原理,也就是CPU是怎么工作的,以及為何這樣設計;學習內存的工作原理,從最基本的電路,到上層抽象給到CPU乃至應用程序的結構是怎樣的;學習CPU是怎么和輸入設備、輸出設備打交道的。
學習組成原理,就是在理解從控制器、運算器、存儲器、輸入設備和輸出設備,從電路這樣的硬件,到最終開放給軟件的接口,是怎么運作的,為什么要設計成這樣,以及在軟件開發(fā)層面盡可能用好它。

在這圖中,整個計算機組成原理拆分為四大部分,分別是計算機的基本組成、計算機的指令和計算、處理器的設計以及存儲器和I/O設備。
“性能”這個詞,不管是在日常生活還是寫程序的時候,都經常被提到。比方說,買新電腦的時候,我們會說“原來的電腦性能跟不上了”;寫程序的時候,我們會說,“這個程序性能需要優(yōu)化一下“。那么,”性能“到底指的是什么?
什么是性能?時間的倒數(shù)
計算機的性能,其實和我們干體力勞動很像,好比是我們要搬東西。對于計算機的性能,我們需要有個衡量標準,這個標準主要有兩個指標。
第一個是響應時間(Response time)或者叫執(zhí)行時間(Execution time)。想要提升響應時間這個性能指標,你可以理解為讓計算機”跑得更快“。

第二個是吞吐率(Throughput)或者帶寬(Bandwidth),想要提升這個指標,你可以理解為讓計算機”搬得更多“。

所以說,響應時間指的就是,我們執(zhí)行一個程序,到底需要花多少時間?;ǖ臅r間越少,自然性能就越好。
而吞吐率就是指我們在一定的時間范圍內,到底能處理多少事情。這里的”事情“,在計算機里就是處理的數(shù)據或者執(zhí)行的程序指令。
和搬東西來做對比,如果我們的響應時間短,跑得快,我們可以來回多跑幾趟多搬幾趟。所以說,縮短程序的響應時間,一般來說都會提升吞吐率。
除了縮短響應時間,我們還有別的方法嗎?當然有,比如說,我們還可以找?guī)讉€人一起來搬,這就類似現(xiàn)代的服務器都是8核、16核的。人多力量大,同時處理數(shù)據,在單位時間內就可以處理更多數(shù)據,吞吐率自然也就上去了。
提升吞吐率的辦法有很多。大部分時候,我們只要多加一些機器,多堆一些硬件就好了。但是響應時間的提升卻沒有那么容易,因為CPU的性能提升其實在10年前就處于”擠牙膏“的狀態(tài)了,所以我們得慎重地來分析對待。下面我們具體來看:
我們一般把性能,定義為響應時間的倒數(shù),也就是
性能 = 1 / 響應時間
這樣一來,響應時間越短,性能的數(shù)值就越大。同樣一個程序,在Inter最新的CPU Coffee Lake上,只需要30s就能運行完成,而在5年前的CPU Sandy Bridge上,需要1min才能完成,那么我們自然可以算出來,Coffee Lake的性能1 / 30,Sandy Bridge的性能是1 / 60,兩個的性能比為2,于是,我們就說,Coffee Lake的性能是Sandy Bridge的2倍。
過去幾年流行的手機跑分軟件,就是把多個預設好的程序在手機上運行,然后根據運行需要的時間,算出一個分數(shù)來給手機的性能評估。而在業(yè)界,各大CPU和服務器廠商組織了一個叫做SPEC的第三方機構,專門用來指定各種”跑分“的規(guī)則。

計算機的計時單位:CPU時鐘
雖然時間是一個很自然的用來衡量性能的指標,但是用時間來衡量,有兩個問題。
第一個是時間不”準“。如果用你自己隨便寫的一個程序,來統(tǒng)計程序運行時間,每一次統(tǒng)計結果不會完全一樣。有可能這一次花了45ms,下一次變成了53ms。
為什么會不準呢?這里面有好幾個原因。首先,我們統(tǒng)計時間是用類似于”掐秒表“一樣,記錄程序運行結束的時間減去程序開始運行的時間。這個時間也叫Wall Clock Time或者Elapsed Time,就是在運行程序期間,掛在墻上的鐘走掉的時間。
但是,計算機可能同時運行著好多程序,CPU實際上不停地在各個程序之間進行切換。在這些走掉的時間里面,很可能CPU切換去運行別的程序了。而且,有些程序運行的時候,可能要從網絡、硬盤去讀取數(shù)據,要等網絡和硬盤把數(shù)據讀出來,給到內存和CPU。所以說,要想準確統(tǒng)計某個程序運行時間,進而去比較兩個程序的實際性能,我們得把這些時間給刨除掉。
那這件事怎么實現(xiàn)呢?Linux下有個叫time的命令,可以幫我們統(tǒng)計出來,同樣的Wall Clock Time下,程序實際在CPU上到底花了多少時間。
我們簡單運行一下time命令。它會返回三個值,第一個是real time,也就是我們說的Wall Clock Time,也就是運行程序整個過程中流逝掉的時間;第二個是user time,也就是CPU在運行你的程序,在用戶態(tài)運行指令的時間;第三個是sys time,是CPU在運行你的程序,在操作系統(tǒng)內核里運行指令的時間。而程序實際花費的CPU執(zhí)行時間(CPU Time),就是user time + sys time
?```
timeseq1000000|wc-l
1000000?
real? 0m0.101s
user? 0m0.031s
sys ? 0m0.016s
```
在這個例子里,可以看到,實際上程序用了0.101s,但是CPU time只有0.031s + 0.014s = 0.047s。運行程序的時間里,只有不到一半是實際花在這個程序上的。

其次,即使我們已經拿到了CPU時間,我們也不一定可以直接”比較“出兩個程序的性能差異。即使在同一臺計算機上,CPU可能滿載運行也可能降頻運行,降頻運行的時候自然話的時間會多一些。
除了CPU之外,時間這個性能指標還會收到主板、內存這些其他相關硬件的影響。所以,我們需要對”時間”這個我們可以感知的指標進行拆解,把程序的CPU執(zhí)行時間變成CPU時鐘周期數(shù)(CPU Cyles)和時鐘周期時間(Clock Cycle)的乘積。
程序的CPU執(zhí)行時間 = CPU時鐘周期數(shù) * 時鐘周期時間
我們先來理解一下什么是時鐘周期時間。你在買電腦的時候,一定關注過CPU的主頻。比如我手頭的這臺電腦就是Inter Core-i7-7700HQ 2.8GHz,這里的2.8GHz就是電腦的主頻(Frequency/Clock Rate)。這個2.8GHz,我們可以先粗淺地認為,CPU在1s時間內,可以執(zhí)行的簡單指令的數(shù)量是2.8G條。
如果想要更準確一點描述,這個2.8GHz就代表,我們CPU的一個“鐘表“能夠識別出來的最小時間間隔。就像我們掛在墻上的掛鐘,都是”滴答滴答“一秒一秒地走,所以通過墻上的掛鐘能夠識別出來的最小時間單位就是秒。
而在CPU內部,和我們平時戴的石英表類似,有一個叫晶體振蕩器(Oscillator Crystal)的東西,簡稱晶振。我們把晶振當成CPU內部的電子表來使用。晶振帶來的每一次”滴答“,就是時鐘周期時間。
在這個2.8GHz的CPU上,這個時鐘周期時間,就是1 / 2.8G。我們的CPU,是按照這個”時鐘“提示的時間來進行自己的操作。主頻越高,意味著這個表走得越快,我們的CPU也就”被逼“著走的越快。
如果你自己組裝過臺式機的話,可能聽說過”超頻“這個概念,著說的其實就相當于把買回來的CPU內部的鐘給調快了,于是CPU的計算跟著這個時鐘的節(jié)奏,也就自然變快了。當然這個快不是沒有代價的,CPU跑得越快,散熱的壓力也就越大。就和人一樣,超過生理極限,CPU就會崩潰。
我們現(xiàn)在回到上面程序CPU執(zhí)行時間的公式。
程序的CPU執(zhí)行時間 = CPU時鐘周期數(shù) * 時鐘周期時間
最簡單的提升性能方案,自然縮短時鐘周期時間,也就是提升主頻。換句話說,就是換一塊好一點的CPU。不過,這個是我們軟件工程師控制不了的事情,所以我們把目光挪到了乘法的另一個因子---CPU時鐘周期數(shù)上。如果能夠減少程序需要的CPU時鐘周期數(shù),一樣能夠提升程序性能。
對于CPU時鐘周期數(shù),我們可以再做一個分解,把它變成”指令數(shù) * 每條指令的平均時鐘周期數(shù)(Cycles Per Instruction,簡稱CPI)“。不同的指令需要的Cycles是不同的,加法和乘法都對應一條CPU指令,但是乘法需要的Cycles就比加法要多,自然也就慢。在這樣拆分之后,我們的程序CPU執(zhí)行時間就可以變成這樣三個部分的乘積。
程序的CPU執(zhí)行時間 = 指令數(shù) * CPI * Clock Cycle Time
因此,如果我們想要解決性能問題,其實就是要優(yōu)化這三者。
1.時鐘周期時間,就是計算機的主頻,這個取決于計算機硬件。我們所熟知的摩爾定律就一直在不停地提高我們計算機的主頻。比如說,最早的80386主頻只有33MHz,現(xiàn)在手頭的筆記本電腦就是2.8GHz,在主頻層面,就提升了將近100倍。
2.每條指令的平均時鐘周期數(shù)CPI,就是一條指令到底需要多少CPU Cycle。在后面講解CPU結構的時候,我們會看到,現(xiàn)代的CPU通過流水線技術(Pipeline),讓一條指令需要的CPU Cycle盡可能的少。因此,對于CPI的優(yōu)化,也是計算機組成和體系結構中重要一環(huán)。
3.指令數(shù),代表執(zhí)行我們程序到底需要多少條指令、用哪些指令。這個很多時候就把挑戰(zhàn)給了編譯器。同樣的代碼,編譯成計算機指令的時候,就有各種不同的表示方式。
我們可以把自己想象成一個CPU,坐在那里寫程序。計算機主頻就好像是你的打字速度,打字越快,你自然可以多寫一點程序。CPI相當于你在寫程序的時候,熟悉各種快捷鍵,越是打相同的內容,需要敲擊鍵盤的次數(shù)就越少。指令數(shù)相當于你的程序設計得夠合理,同樣的程序要寫的代碼行數(shù)就少。如果三者皆能實現(xiàn),你自然可以很快地寫出一個優(yōu)秀的程序,你的”性能“從外面來看就是好的。
在上一講,提到了這樣一個公式:程序的CPU執(zhí)行時間 = 指令數(shù) * CPI * Clock Cycle Time這么看來,如果要提升計算機的性能,我們可以從指令數(shù)、CPI以及CPU主頻這三個地方入手。要搞定指令數(shù)或者CPI,乍一看都不太容易。于是,研發(fā)CPU的硬件工程師們,從80年代開始,就挑上了CPU這個“軟柿子”。在CPU上多放一點晶體管,不斷提升CPU的時鐘頻率,這樣就能讓CPU變得更快,程序的執(zhí)行時間就會縮短。于是,從1978年Intel發(fā)布的8086CPU開始,計算機的主頻從5MHz開始,不斷提升。1980年代中期的80386能夠跑到40MHz,1989年的486能夠跑到100MHz,直到2000年的奔騰4處理器,主頻已經到達了1.4GHz。而消費者也在這20年里養(yǎng)成了“看主頻”買電腦的習慣。當時已經基本壟斷了桌面CPU市場的Intel更是夸下了海口,表示奔騰4所使用的CPU結構可以做到10GHz,頗有“大力出奇跡”的意思。功耗:CPU的“人體極限”然而,計算機科學界從來不相信“大力出奇跡”。奔騰4的CPU主頻從來沒有達到過10GHz,最終它的主頻上限定格在3.8GHz。這還不是最糟的,更糟糕的事情是,大家發(fā)現(xiàn),奔騰4的主頻雖然高,但是它的實際性能卻配不上同樣的主頻。想要用在筆記本上的奔騰4 2.4GHz處理器,其性能之和基于奔騰3架構的奔騰M 1.6GHz處理器差不多。于是,這一次的“大力出悲劇”,不僅讓Intel的對手AMD獲得了喘息之機,更是代表著“主頻時代”的終結。后面幾代Intel CPU主頻不但沒有上升,反而下降了。到如今,2019年的最高配置Intel i9 CPU,主頻也之不過是5GHz而已。相較于1978年到2000年,這20年里300倍的主頻提升,從2000年到現(xiàn)在的這19年,CPU的主頻大概提高了3倍。

奔騰4的主頻為什么沒能超過3.8GHz的障礙呢?答案就是功耗問題。什么功耗問題呢?我們先看一個直觀的例子。
一個3.8GHz的奔騰4處理器,滿載功率是130瓦。這個130瓦是什么概念呢?機場允許帶上飛機的充電寶的容量上限是100瓦。如果我們把這個CPU安在手機里面,不考慮屏幕內存之類的耗電,這個CPU滿載運行45分鐘,充電寶里面就沒電了。而iphone x使用ARM架構的CPU,功率則只有4.5瓦左右。
我們的CPU,一般都被叫做超大規(guī)模集成電路(Very-Large-Scale Intergration,VLSI)。這些電路,實際上都是一個個晶體管組合而成的。CPU在計算,其實就是讓晶體管里面的“開關”不斷地去“打開”和“關閉”,來組合完成各種運算和功能。
想要計算得快,一方面,我們要在CPU里,同樣的面積里面,多放一些晶體管,也就是增加密度;另一方面,我們要讓晶體管“打開”和“關閉”得更快一點,也就是提升主頻。而這兩者,都會增加功耗,帶來耗電和散熱的問題。
這么說,可能還是有點抽象,我還是舉一個例子。你可以把一個計算機CPU想象成一個巨大的工廠,里面有很多工人,相當于CPU上面的晶體管,互相之間協(xié)同工作。
為了工作得快一點,我們要在工廠里多塞一點人。你可能會問,為什么不把工廠造得大一點呢?這時因為,人和人之間如果離得遠了,互相之間走過去需要花費的時間就變長,這也會導致性能的下降。這就好像如果CPU的面積大,晶體管之間的距離變大,電信號傳輸?shù)臅r間就會邊長,運算速度自然就慢了。
除了多塞一點人,我們還希望每個人的動作都快一點,這樣同樣的時間里就可以多干一點活了。這就相當于提升CPU的主頻,但是動作快,每個人就要出汗散熱。要是太熱了,對工廠里面的人來說就會中暑生病,對CPU來說就會崩潰出錯。
我們會在CPU上面抹硅脂、裝電扇,乃至用上水冷或者其他更好的散熱設備,就好像在工廠里面裝風扇、空調,發(fā)冷飲一樣。但是同樣的空間下,裝上風扇空調能夠帶來的散熱效果也是有極限的。
因此,在CPU里面,能夠放下的晶體管數(shù)量和晶體管的“開關”頻率也都是有限的。一個CPU的功率,可以用這樣一個公式來表示:
功耗 ~= 1 / 2 * 負載電容 * 電壓的平方 * 開關頻率 * 晶體管數(shù)量
那么,為了要提高性能,我們需要不斷地增加晶體管的數(shù)量。同樣的面積下,我們想要多放一點晶體管,就要把晶體管造得小一點。這個就是平時我們所說的提升“制程”。從28nm到7nm,相當于晶體管本身變成了原來的1 / 4大小。這個就相當于我們在工廠里,同樣的活,我們要找瘦小一點的工人,這樣一個工廠里面就可以多一些人。我們還要提升主頻,讓開關的頻率變快,也就是要找手腳更快的工人。

但是,功耗增加太多,就會導致CPU散熱跟不上,這時,我們就需要降低電壓。這里有一點非常關鍵,在整個功耗的公式里面,功耗和電壓的平方成正比。這意味著電壓下降到原來的1 / 5,整個的功耗會變成原來的1 / 25。
事實上,從5MHz主頻的8086到5GHz的Intel i9,CPU的電壓已經從5V左右下降到1V左右。這也是為什么我們CPU的主頻提升了1000倍,但是功耗只增加了40倍。
并行優(yōu)化,理解阿姆達爾定律
雖然制程的優(yōu)化和電壓的下降,在過去的 20 年里,讓我們的 CPU 性能有所提升。但是從上世紀九十年代到本世紀初,軟件工程師們所用的“面向摩爾定律編程”的套路越來越用不下去了?!皩懗绦虿豢紤]性能,等明年 CPU 性能提升一倍,到時候性能自然就不成問題了”,這種想法已經不可行了。于是,從奔騰 4 開始,Intel 意識到通過提升主頻比較“難”去實現(xiàn)性能提升,邊開始推出 Core Duo 這樣的多核 CPU,通過提升“吞吐率”而不是“響應時間”,來達到目的。提升響應時間,就好比提升你用的交通工具的速度,比如原本你是開汽車,現(xiàn)在變成了火車乃至飛機。本來開車從上海到北京要 20 個小時,換成飛機就只要 2 個小時了,但是,在此之上,再想要提升速度就不太容易了。我們的 CPU 在奔騰 4 的年代,就好比已經到了飛機這個速度極限。
那你可能要問了,接下來該怎么辦呢?相比于給飛機提速,工程師們又想到了新的辦法,可以一次同時開 2 架、4 架乃至 8 架飛機,這就好像我們現(xiàn)在用的 2 核、4 核,乃至 8 核的 CPU。雖然從上海到北京的時間沒有變,但是一次飛 8 架飛機能夠運的東西自然就變多了,也就是所謂的“吞吐率”變大了。所以,不管你有沒有需要,現(xiàn)在 CPU 的性能就是提升了 2 倍乃至 8 倍、16 倍。這也是一個最常見的提升性能的方式,通過并行提高性能。這個思想在很多地方都可以使用。舉個例子,我們做機器學習程序的時候,需要計算向量的點積,比如向量 W=[W0,W1,W2,…,W15] 和向量 X=[X0,X1,X2,…,X15],W?X=W0?X0+W1?X1+ W2?X2+…+W15?X15。這些式子由 16 個乘法和 1 個連加組成。如果你自己一個人用筆來算的話,需要一步一步算 16 次乘法和 15 次加法。如果這個時候我們把這個任務分配給 4 個人,同時去算 W0~W3, W4~W7, W8~W11, W12~W15 這樣四個部分的結果,再由一個人進行匯總,需要的時間就會縮短。

但是,并不是所有問題,都可以通過并行提高性能來解決。如果想要使用這種思想,需要滿足這樣幾個條件。第一,需要進行的計算,本身可以分解成幾個可以并行的任務。好比上面的乘法和加法計算,幾個人可以同時進行,不會影響最后的結果。第二,需要能夠分解好問題,并確保幾個人的結果能夠匯總到一起。第三,在“匯總”這個階段,是沒有辦法并行進行的,還是得順序執(zhí)行,一步一步來。這就引出了我們在進行性能優(yōu)化中,常常用到的一個經驗定律,阿姆達爾定律(Amdahl’s Law)。這個定律說的就是,對于一個程序進行優(yōu)化之后,處理器并行運算之后效率提升的情況。具體可以用這樣一個公式來表示:優(yōu)化后的執(zhí)行時間 = 受優(yōu)化影響的執(zhí)行時間 / 加速倍數(shù) + 不受影響的執(zhí)行時間
在剛剛的向量點積例子里,4 個人同時計算向量的一小段點積,就是通過并行提高了這部分的計算性能。但是,這 4 個人的計算結果,最終還是要在一個人那里進行匯總相加。這部分匯總相加的時間,是不能通過并行來優(yōu)化的,也就是上面的公式里面不受影響的執(zhí)行時間這一部分。比如上面的各個向量的一小段的點積,需要 100ns,加法需要 20ns,總共需要 120ns。這里通過并行 4 個 CPU 有了 4 倍的加速度。那么最終優(yōu)化后,就有了 100/4+20=45ns。即使我們增加更多的并行度來提供加速倍數(shù),比如有 100 個 CPU,整個時間也需要 100/100+20=21ns。

總結延伸我們可以看到,無論是簡單地通過提升主頻,還是增加更多的 CPU 核心數(shù)量,通過并行來提升性能,都會遇到相應的瓶頸。僅僅簡單地通過“堆硬件”的方式,在今天已經不能很好地滿足我們對于程序性能的期望了。于是,工程師們需要從其他方面開始下功夫了。在“摩爾定律”和“并行計算”之外,在整個計算機組成層面,還有這樣幾個原則性的性能提升方法。1.加速大概率事件。最典型的就是,過去幾年流行的深度學習,整個計算過程中,99% 都是向量和矩陣計算,于是,工程師們通過用 GPU 替代 CPU,大幅度提升了深度學習的模型訓練過程。本來一個 CPU 需要跑幾小時甚至幾天的程序,GPU 只需要幾分鐘就好了。Google 更是不滿足于 GPU 的性能,進一步地推出了 TPU。后面的文章,我也會為你講解 GPU 和 TPU 的基本構造和原理。
2.通過流水線提高性能?,F(xiàn)代的工廠里的生產線叫“流水線”。我們可以把裝配 iPhone 這樣的任務拆分成一個個細分的任務,讓每個人都只需要處理一道工序,最大化整個工廠的生產效率。類似的,我們的 CPU 其實就是一個“運算工廠”。我們把 CPU 指令執(zhí)行的過程進行拆分,細化運行,也是現(xiàn)代 CPU 在主頻沒有辦法提升那么多的情況下,性能仍然可以得到提升的重要原因之一。我們在后面也會講到,現(xiàn)代 CPU 里是如何通過流水線來提升性能的,以及反面的,過長的流水線會帶來什么新的功耗和效率上的負面影響。3.通過預測提高性能。通過預先猜測下一步該干什么,而不是等上一步運行的結果,提前進行運算,也是讓程序跑得更快一點的辦法。典型的例子就是在一個循環(huán)訪問數(shù)組的時候,憑經驗,你也會猜到下一步我們會訪問數(shù)組的下一項。后面要講的“分支和冒險”、“局部性原理”這些 CPU 和存儲系統(tǒng)設計方法,其實都是在利用我們對于未來的“預測”,提前進行相應的操作,來提升我們的程序性能。好了,到這里,我們講完了計算機組成原理這門課的“前情提要”。一方面,整個組成乃至體系結構,都是基于馮·諾依曼架構組成的軟硬件一體的解決方案。另一方面,你需要明白的就是,這里面的方方面面的設計和考慮,除了體系結構層面的抽象和通用性之外,核心需要考慮的是“性能”問題。