指令也是資料?淺談計算機體系結構

語言: CN / TW / HK

theme: condensed-night-purple

我從大學期間開始接觸程式設計,接觸的第一門程式語言是C語言,後來也進過實驗室,玩過微控制器,還接觸過彙編,但是最終都沒有走下去。到大四下學期的時候由於畢業設計和工作的需要,開始接觸JavaWeb,從此踏上了一條不歸路。

現在也已經工作兩年多的時間了,接觸的語言也越來越多,我也成功脫離的語言之爭的低階趣味。學習更多的程式語言,熟練使用甚至精通多門程式語言已經成為了我長期的目標。

不知為何,我對程式設計有一種特別的執念,總想總結出一套自己的方法論和思維體系,在編碼的時候總想以最優雅的方式完成邏輯,可能我本身就覺得寫程式碼是件非常酷的事情吧。

因此我覺得有必要寫一下我對程式語言的理解,為我以後學習多門語言打下思維基礎。

在進入正題之前,我們有必要從故事開始的地方,捋一下軟體開發是如何發展到今天的局面的——無論我們使用什麼樣的程式語言,也無論我們編寫要在什麼平臺上執行的程式,程式碼的編寫方式都是大差不差的。

這就不得不提計算機的體系結構。

最早的計算機僅內涵固定用途的程式,其運算邏輯被固化在了電路中,只能用於特定的用途,而不是像現在這樣的通用計算機。也就是早期的計算機並沒有那麼“可程式設計”,當時所謂的“重寫程式”很可能指的是紙筆設計程式步驟,接著制定工程細節,再施工將機器的電路配線或結構改變。

典型的就是早年的“Plugboard”這樣的插線板式的計算機。整個計算機就是一個巨大的插線板,通過在板子上不同的插頭或者介面的位置插入線路,來實現不同的功能。這樣的計算機自然是“可程式設計”的,但是編寫好的程式不能儲存下來供下一次載入使用,不得不每次要用到和當前不同的“程式”的時候,重新插板子,重新“程式設計”。

Plugboard

現代的某些計算機仍然維持這樣的設計方式,通常是為了簡化或教育目的。例如一個計算器僅有固定的數學計算程式,它不能拿來做文書處理,更不能拿來玩遊戲。如果想要改變此機器的程式,你必須更改線路、更改結構甚至是重新設計此機器。它的核心是運算電路,而控制電路則只是起到了輔助的作用,控制電路只是負責將電訊號導向適合的運算電路,其執行流程基本是固化的,這樣的計算機是不可程式設計的。

馮·諾依曼體系結構

可以看到,無論是“不可程式設計”還是“不可儲存”,都會讓使用計算機的效率大大下降,因為其大大降低了計算機的通用性。在今天,計算機裝置可以說是無處不在的,其存在形式同樣也是多種多樣的,對於進行應用程式開發的程式設計師來說,接觸的最多的無非就是伺服器、個人PC和手機。但是,無論是我們編寫執行在伺服器上的應用程式後臺,還是編寫在PC上的客戶端亦或者是編寫手機上的APP,我們的編碼方式其實都是大差不差的,這就意味著,不論是什麼樣形式的計算機,其都遵循了一個統一的邏輯模型,這就是計算機祖師爺之一馮·諾依曼所提出的馮·諾依曼體系結構,因為此結構隱約指導了將儲存裝置與中央處理器分開的概念,因此依據本結構設計出的計算機也叫儲存程式計算機。

我們的馮祖師爺,基於當時在祕密開發的EDVAC寫了一篇報告'First Draft of a Report on the EDVAC',簡稱叫 First Draft,翻譯成中文,其實就是《第一份草案》。在這篇文章中,馮·諾依曼描述了他心目中的一臺計算機應該長什麼樣,我們一起來看看。

  • 首先是一個包含算術邏輯單元(Arithmetic Logic Unit,ALU)和處理器暫存器(Processor Register)的處理器單元(Processing Unit),用來完成各種算術和邏輯運算。
  • 然後是一個包含指令暫存器(Instruction Register)和程式計數器(Program Counter)的控制器單元(Control Unit/CU),用來控制程式的流程,通常就是不同條件下的分支和跳轉。在現在的計算機裡,上面的算術邏輯單元和這裡的控制器單元,共同組成了我們說的 CPU。
  • 接著是用來儲存資料(Data)和指令(Instruction)的記憶體。以及更大容量的外部儲存,在過去,可能是磁帶、磁鼓這樣的裝置,現在通常就是硬碟。
  • 最後就是各種輸入和輸出裝置,以及對應的輸入和輸出機制。我們現在無論是使用什麼樣的計算機,其實都是和輸入輸出裝置在打交道。個人電腦的滑鼠鍵盤是輸入裝置,顯示器是輸出裝置。我們用的智慧手機,觸控式螢幕既是輸入裝置,又是輸出裝置。而跑在各種雲上的伺服器,則是通過網路來進行輸入和輸出。這個時候,網絡卡既是輸入裝置又是輸出裝置。

任何一臺計算機的任何一個部件都可以歸到運算器、控制器、儲存器、輸入裝置和輸出裝置中,所有的現代計算機也都是基於這個基礎架構來設計開發的。而所有的計算機程式,也都可以抽象為從輸入裝置讀取輸入資訊,通過運算器和控制器來執行儲存在儲存器裡的程式,最終把結果輸出到輸出裝置中。而我們編寫的所有的無論高階還是低階語言的程式,也都是基於這樣一個抽象框架來進行運作的。

馮·諾依曼體系結構概念圖

馮·諾依曼體系結構的特點

馮·諾依曼體系結構有一個非常明顯的特點——它一視同仁地對待指令和資料,它規定了指令和資料統一用二進位制進行編碼,而且指令和資料是儲存在同一個儲存器中(同一個記憶體空間中)的,藉著將指令當成一種特別型別的靜態資料,一臺儲存程式型電腦可輕易改變其程式,並在程控下改變其運算內容。

儲存程式型概念也可讓程式執行時自我修改程式的運算內容。本概念的設計動機之一就是可讓程式自行增加內容或改變程式指令的儲存器位置,因為早期的設計都要使用者手動修改。但隨著變址暫存器與間接位置訪問變成硬體結構的必備機制後,本功能就不如以往重要了。而程式自我修改這項特色也被現代程式設計所棄揚,因為它會造成理解與除錯的難度,且現代中央處理器的管線與快取機制會讓此功能效率降低。

---------維基百科

雖然指令和資料共用同一個記憶體空間,但是我們在程式碼中組織記憶體的時候指令和資料卻不是一鍋粥式地儲存在一起,而是採用了分段的方式,最典型的就是將程式分為四段,分別是程式碼段、靜態儲存區、堆區(自由區,動態儲存區)和棧。

雖然我們上文中引用的維基百科中的在程式執行過程中動態修改指令資料的方式已經基本被拋棄,但是在程式的執行過程中在堆區生成代表指令的資料同時將程式計數器重定向到堆區上的方式還是比較常用的(解釋型語言的直譯器、人工智慧等場景),通過這樣的方式我們同樣可以實現在執行時生成指令的目的。

馮·諾依曼瓶頸

將CPU與儲存器分開並非十全十美,反而會導致所謂的馮·諾伊曼瓶頸(von Neumann bottleneck):在CPU與儲存器之間的流量(資料傳輸率)與儲存器的容量相比起來相當小,在現代電腦中,流量與CPU的工作效率相比之下非常小,在某些情況下(當CPU需要在巨大的資料上執行一些簡單指令時),資料流量就成了整體效率非常嚴重的限制。CPU將會在資料輸入或輸出儲存器時閒置。由於CPU速度遠大於儲存器讀寫速率,因此瓶頸問題越來越嚴重。

------------維基百科

現代的計算機通常採用多級儲存器(也就是快取)來解決馮·諾依曼瓶頸,對CPU進行流水線設計以及加入分支預測功能也幫助緩和了此問題。同時引入快取使得我們操作記憶體的粒度由逐位元組擴大到了逐快取行(比較大的快取設計可能一個快取行能容納32個位元組),這也在極大程度上減少了記憶體訪問的次數,緩解了瓶頸。

馮·諾依曼體系結構還存在一個問題,因為指令和資料共享同一個記憶體空間,那麼CPU的取指令操作和資料讀寫操作有可能就會發生資源競爭,這也在一定程度上加劇了瓶頸。

哈弗架構

從嚴格意義上說,哈弗架構並沒有完全跳出馮·諾依曼體系結構,它也是一種程式儲存計算機,只是其在指令和資料的儲存方式上採用了和馮·諾依曼不一樣的方式——把程式指令和資料分開儲存。CPU首先到程式指令儲存器中讀取程式指令內容,解碼後得到資料地址,再到相應的資料儲存器中讀取資料,並進行下一步的操作(通常是執行)。程式指令儲存和資料儲存分開,資料和指令的儲存可以同時進行,可以使指令和資料有不同的資料寬度,哈佛架構的微處理器通常具有較高的執行效率。其程式指令和資料指令分開組織和儲存的,執行時可以預先讀取下一條指令。

哈弗結構邏輯圖

這樣的設計方式解決了CPU取指令和記憶體讀寫的資源競爭,但是電路設計上可能會更加複雜——因為可以使指令和資料有不同的資料寬度,可能要單獨設計兩套儲存器訪問的電路;也正是因為指令和資料的寬度可能不一致,所以指令和資料雖然在物理上還是同構的(都是用二進位制編碼儲存的),但是在邏輯上已經是異構的了,那麼如果要實現我們上文所說的在執行時動態生成代表指令的資料的功能就要花費額外的精力。

基於上面的限制,哈弗架構並沒有得到非常廣泛的應用,而為了解決CPU取指令和資料讀寫的資源競爭,現在大多數的計算機都採用了我們下文要介紹的改進的哈弗架構。

改進的哈弗架構

改進的哈弗結構其實是綜合了哈弗結構和馮·諾依曼結構的特點,其還是把指令和資料進行統一的儲存,但是在快取的層面做了一些文章——把快取拆分為了指令快取和資料快取,但它放鬆了指令和資料之間嚴格分離的這一特徵,仍然允許CPU同時訪問兩個(或更多)記憶體匯流排。

下圖是我擷取的我的電腦的CPU資訊,其中一級快取(也就是最靠近CPU暫存器的那層快取)已經分為了指令快取和資料快取,但是在下層的快取中則沒有這樣的區分,當然了,記憶體也是沒有這樣的區分的。

image-20200524222017382

也就是說,改進的哈弗架構把哈弗架構的特性放到了更高層級的儲存中,而在最底層的儲存中仍然保留了馮·諾依曼體系結構的特點。

現代高效能CPU晶片在設計上包含了哈佛和馮諾依曼結構的特點。特別是,“拆分快取”這種改進型的哈佛架構版本是很常見的。CPU的快取分為指令快取和資料快取。CPU訪問快取時使用哈佛體系結構。然而當快取記憶體未命中時,資料從主儲存器中檢索,卻並不分為獨立的指令和資料部分,雖然它有獨立的記憶體控制器用於訪問RAM,ROM和(NOR)快閃記憶體。

因此,在一些情況下可以看到馮諾依曼架構,比如當資料和程式碼通過相同的記憶體控制器時,這種硬體通過哈佛架構在快取訪問或至少主記憶體訪問方面提高了執行效率。此外,在寫非快取區之後,CPU經常擁有寫快取使CPU可以繼續執行。當指令被CPU當作資料寫入,且軟體必須確保在試圖執行這些剛寫入的指令之前,快取記憶體(指令和資料)和寫快取是同步的,這時馮諾依曼結構的記憶體特點就出現了。

-------維基百科

指令也是資料

現代計算機體系結構,無論是馮·諾依曼體系還是哈弗架構,都把會把我們的程式儲存起來,以此來實現計算機的“可程式設計”,其實這裡面最本質的思維是吧指令當做資料來對待。

指令和資料是同構的,理解這一點是非常重要的,因為現在又非常多的程式都是依賴於這個理論基礎的,比如某些解釋型語言的直譯器就是在解釋程式碼的時候生成可供計算機直接執行的機器碼,然後把CPU的指令計數器指向剛剛生成的機器碼進行執行。即使是以後出現了顛覆馮·諾依曼體系結構的新計算機體系結構,其也應該儘量實現這一點。即便是純粹的哈弗架構,其也應該提供相應的指令來實現這樣的同構。

上文中我們提到了機器碼,也提到了現代計算機體系結構統一用二進位制儲存指令和資料,想必在你剛剛接觸程式設計的時候就知道了,計算機只認0101這樣的機器碼,現在我們對支援我們使用高階語言程式設計的硬體環境有了基本的瞭解,那麼程式語言又是如何一步一步從0101這樣的機器碼自舉到今天高階語言遍地開花的局面的呢?下一篇文章,我們就來介紹一下高階語言的誕生。

感謝你耐心讀完。本人深知技術水平和表達能力有限,如果文中有什麼地方不合理或者你有其他不同的思考和看法,歡迎隨時和我進行討論([email protected])。