一個小小的循環,重複上億次
上一篇我們看了把應用程式變成電流的抽象層,再之前看了馮諾依曼模型:一個控制單元加一條資料路徑,接到同時存放程式與資料的記憶體。現在我們放大這台機器一刻一刻的生活。剝開作業系統、編譯器、上億顆電晶體,處理器底層其實只是在一個緊湊的循環裡做一件出奇簡單的事。它沒有在思考,也沒有在推理,它只是把三個步驟重複得太快,快到結果看起來像是智慧。
這三個步驟就是提取、解碼和執行——合起來就是指令循環。提取:到記憶體去抓下一條指令。解碼:弄清它是哪一類指令、需要什麼。執行:真正去做它——把兩個數相加、載入一個值、跳到別處去。然後循環從頭再來,換下一條指令。就這樣。現代 CPU 每秒把這個循環跑上億次,而你用過的每一個程式——一款遊戲、一個瀏覽器、乃至這個頁面——都不過是一長串指令,被一口一口地嚼完。
程式計數器:指著你正在讀的那一行的手指
CPU 怎麼知道哪一條指令是下一條?它用一個特別的暫存器,叫做程式計數器(PC),裡面存著下一條要提取的指令的記憶體位址。把它想成你沿著食譜某一行劃過去、好讓自己不迷失位置的那根手指:它總是指著你即將要做的那一步。每條指令都住在記憶體裡的某個位址,就像房子有門牌號碼,而 PC 就只是存著即將輪到的那一條的位址。
每次提取之後,PC 會悄悄前進到下一條指令——通常就是把它目前的值加上一條指令的大小。如果指令是四個位元組寬(RISC-V 機器常見的選擇),PC 每次就往前跨 4,沿著記憶體一路直走。正是這穩定的行進,讓程式預設由上而下執行。那些有趣的情形——迴圈、if 判斷、函式呼叫——恰恰就是有東西覆寫 PC、塞進一個不同的位址、不讓它乖乖加 4 的時刻。一次分支,說穿了,不過就是對程式計數器寫入一個新值而已。
把一條指令完整地走一遍
讓我們把一條指令從頭到尾追一遍,好讓這個循環變得具體。假設下一條指令是「把暫存器 x5 和 x6 裡的數相加,結果放進 x7」。控制單元會編排一連串固定的動作,在正確的時刻拉動資料路徑裡正確的拉桿。下面就是那串順序——幾乎對每條指令都是同一個樣子,只有執行那一步會隨著指令要做什麼而改變。
- 提取:讀取存放在程式計數器所指位址的那條指令,把它的位元從記憶體拉進 CPU。接著把 PC 往前推(比方說 +4),讓它已經指向再下一條指令。
- 解碼:控制單元檢視指令的位元——它的運算碼(那個說「這是一次加法」的欄位)以及指名 x5、x6、x7 的欄位。它從暫存器檔讀出 x5 和 x6 目前的值。至此機器已清楚知道要做什麼、對誰做。
- 執行:算術邏輯單元(ALU)真正把兩個值相加。這一步會因指令而異——換成載入指令,這裡會改成算出一個記憶體位址;換成分支指令,則會比較兩個值。
- 寫回:把和寫進暫存器 x7,程式之後就能用到它。一輪結束;控制單元帶著已經更新好的 PC 回到第 1 步。
注意這當中有多少工作不論哪條指令都一樣:對加法、載入或分支來說,提取和解碼幾乎完全相同。這份相同性對硬體設計者是一份禮物。因為前面那些階段不取決於指令做什麼,它們可以只造一次、給每條指令重複使用——而且,正如我們在後面的階梯會看到的,它們還能像生產線一樣在多條指令之間彼此重疊,這正是管線的由來。
時脈:讓所有人步調一致的鼓點
是什麼攔住 CPU 別衝太快、別在 ALU 還沒寫完之前就去讀某個暫存器?一個叫做時脈的心跳。硬體裡有個小小的振盪器,以固定的速率滴答,每一個時脈週期就是那面鼓的一次敲擊。每敲一下,已經穩定下來的結果才獲准往前送進下一個階段;兩次敲擊之間,邏輯電路忙著計算,但什麼都還沒定案。正是時脈讓成千上萬個微小操作齊步前進,而不是互相踩踏。
每秒滴答的次數就是時脈速率——3 GHz 表示每秒滴答三十億次。人很容易把它讀成純粹的快慢,但這裡要給出這架階梯的第一個誠實警告:光是時脈速率比較高,並不代表電腦比較快。這就是有名的「百萬赫茲迷思」(megahertz myth)。一顆滴答更快、卻每條指令要花更多次滴答、或跑著更笨拙程式的晶片,輕易就會輸給滴答較慢的對手。快慢從來不是單一個數字。
從循環走到效能的鐵律
既然你已經看著一條指令爬過了整個循環,一條真正的效能公式就直接從中掉了出來。一個程式在 CPU 上花的總時間,不過就是它跑了多少條指令、乘以每條平均要花多少個週期、再乘以每個週期持續多久。這個乘積就是效能鐵律,它是整個領域裡最有用的一條式子:
CPU time = instruction count x CPI x cycle time | | | | | how many instructions | seconds per tick (= 1 / clock rate) | the program runs | | average cycles | per instruction total seconds on the CPU
這三項裡的每一項,都是設計者(或程式設計者)能轉動的一個旋鈕,而鐵律告訴你:沒有任何單獨一項能決定勝負。把時脈速率催高會縮短週期時間,卻對指令數或 CPI 毫無影響——若機器其餘部分跟不上,甚至可能把 CPI 推高。更聰明的編譯器能砍掉指令數。管線(後面的階梯會講)能壓低有效 CPI。計算機結構的藝術,就在於同時平衡這三者,而它最古老的格言之一便是讓常見情況變快:把你的力氣花在加速那些真正最常跑到的指令上。
而這裡有個更深的理由,說明鐵律為何在過去二十年主宰了整個領域。有很長一段時間,硬體幾乎免費地變快:每隔幾年電晶體就縮小、時脈速率攀升,同一個程式就自己跑得更快。但大約在 2005 年,這頓免費的午餐結束了——晶片撞上了功耗與發熱的牆,時脈速率不再攀升,週期時間這個旋鈕大致卡死了。一個旋鈕卡住,設計者只好轉而去攻指令數與 CPI,並讓許多週期在多個核心上平行地跑。正是這一次轉折,就是這架階梯後面之所以存在的原因:快取、管線、平行與加速器,全都是這個領域對「時脈不再自己變快」所給出的回答。