關注我們 設為星標
EETOP
百萬芯片工程師專業技術論壇
官方微信號
1985 年,英特爾推出了具有開創性意義的 386 處理器,到今年正好40周年。
它是 x86 架構中的首款 32 位處理器。為提升性能,386 配備了一個 16 字節的指令預取隊列。預取隊列的作用是在指令需要執行前就從內存中獲取它們,這樣處理器在執行指令時通常無需等待內存讀取。指令預取利用了處理器 “思考”(即執行運算等不占用內存總線操作)且內存總線閑置的時間窗口。
在本文中,我將詳細剖析 386 的預取隊列電路。其中一個有趣的電路是增量器,它給一個指針加 1,以按順序遍歷內存。這聽起來好像很簡單,但為了實現高性能,增量器采用了復雜的電路設計。預取隊列使用一個大型電路網絡來對字節進行移位操作,確保它們能正確對齊。它還設有一個簡潔的電路,用于將有符號的 8 位和 16 位數字擴展為 32 位。本文雖不會有重大發現,但如果你對底層電路和動態邏輯感興趣,不妨繼續閱讀。
下圖展示了在顯微鏡下,386 那如指甲蓋大小、閃閃發亮的硅芯片。盡管它看起來可能像一座分區奇特城市的航拍圖,但芯片照片揭示了芯片的功能模塊。左上角的預取單元(Prefetch Unit)就是我們關注的模塊。
在本文中,我將討論預取隊列電路(用紅色突出顯示部分),暫不涉及右側的預取控制電路。預取單元從與內存通信的總線接口單元(Bus Interface Unit,右上角)接收數據。指令解碼單元(Instruction Decode Unit)則逐個字節地從預取單元接收預取指令,并對操作碼進行解碼,以便執行。
這張 386 的芯片裸片照片顯示了寄存器的位置
芯片的左四分之一部分由一條條電路組成,這些電路看起來比芯片其他部分規整得多。這種類似網格的外觀源于每個功能模塊(在很大程度上)都是通過將相同電路重復 32 次構建而成的,每個位對應一次,并排排列。垂直數據線以 32 位為一組上下延伸,連接各個功能模塊。為了實現這一點,每個電路在芯片上必須適配相同的寬度;這種布局限制迫使電路設計師開發出一種能高效利用該寬度且不超出允許寬度的電路。預取隊列的電路采用了相同方法:每個電路寬 66 微米,重復 32 次。正如后文將看到的,要把預取電路塞進這個固定寬度,需要一些布局技巧。
預取器的工作內容
預取單元的目的是通過在指令需要執行前從內存中讀取它們來加快性能,這樣處理器就無需等待從內存獲取指令。預取利用了內存總線閑置的時間,盡量減少與其他讀寫數據指令的沖突。在 386 中,預取的指令存儲在一個 16 字節的隊列中,該隊列由四個 32 位塊組成。
下圖放大展示了預取器及其主要組件。你可以看到,相同的電路(在大多數情況下)是如何重復 32 次,形成垂直條帶的。頂部是來自總線接口單元的 32 條總線線路。這些線路通過總線接口單元在數據通路和外部內存之間建立連接。當右側的 32 條水平線分支并形成 32 條垂直線(每個位對應一條)時,這些線路形成一個三角形圖案。接下來是取指指針(fetch pointer)和限制寄存器(limit register),以及一個用于檢查取指指針是否已到達限制的電路。注意,增量器和限制檢查電路的兩個低階位(右側)的電路缺失了。在增量器的底部,你可以看到某些位位置的電路與其他位置相比,有一塊缺失,打破了重復塊的圖案。16 字節的預取隊列在增量器下方。盡管這個內存是預取器的核心,但它的電路占用的面積相對較小。
預取器的特寫,標注了主要模塊。在右側,預取器接收控制信號
預取器的底部會根據需要對數據進行移位以對齊。一個 32 位的值可能會跨預取緩沖區的兩個 32 位行拆分存儲。為處理這種情況,預取器包含一個數據移位網絡來對數據進行移位和對齊。這個網絡占用大量空間,但這里沒有有源電路:只有水平和垂直導線組成的網格。
最后,符號擴展電路(sign extend circuitry)會根據需要將有符號的 8 位或 16 位值轉換為有符號的 16 位或 32 位值。可以看到,符號擴展電路非常不規則,尤其是在中間部分。一個鎖存器存儲預取隊列的輸出,供數據通路的其他部分使用。
限制檢查
如果你編寫過 x86 程序,可能知道處理器的指令指針(EIP),它存儲著下一條要執行指令的地址。在程序執行時,指令指針會從一條指令指向下一條指令。然而,事實證明,指令指針實際上并不 “存在”!相反,386 有一個 “提前指令取指指針”(Advance Instruction Fetch Pointer),它存儲著下一條要預取到預取隊列中的指令地址。但有時處理器需要知道指令指針的值,例如,在調用子例程時確定返回地址,或者計算相對跳轉的目標地址。那該怎么辦呢?處理器從預取隊列電路中獲取提前指令取指指針地址,然后減去預取隊列當前的長度。結果就是下一條要執行指令的地址,即所需的指令指針值。
提前指令取指指針(即下一條要預取指令的地址)存儲在預取隊列電路頂部的一個寄存器中。隨著指令被預取,這個指針會由預取電路遞增。(由于每次取指 32 位,所以這個指針每次遞增 4,且最低兩位始終為 0。)
但是什么阻止預取器預取過度,超出有效內存范圍呢?x86 架構因使用段來定義內存的有效區域而聞名。一個段有起始和結束地址(稱為基地址和限制地址),通過阻止對段外的訪問來保護內存。386 有六個活動段;相關的是存儲程序指令的代碼段(Code Segment)。因此,代碼段的限制地址控制著預取器何時必須停止預取。預取隊列包含一個電路,當取指指針到達代碼段的限制時,該電路會停止預取。在本節中,我將描述這個電路。
比較兩個值看似簡單,但 386 使用了一些技巧來加快比較速度。基本思路是使用 30 個異或(XOR)門來比較兩個寄存器的位。(為什么是 30 位而不是 32 位呢?因為每次取 32 位,地址的最低兩位是 00,可以忽略。)如果兩個寄存器匹配,所有異或結果都為 0;如果不匹配,至少有一個異或結果為 1。從概念上講,將異或門的輸出連接到一個 32 輸入的或(OR)門,就能得到期望的結果:如果所有位都匹配,輸出為 0;如果存在不匹配,輸出為 1。但遺憾的是,出于電學原因,使用標準 CMOS 邏輯構建一個 32 輸入的或門是不現實的,而且它的體積大到難以融入電路。相反,386 使用動態邏輯來實現一個分散的或非(NOR)門,預取器的每一列中有一個晶體管。
下圖展示了一位相等比較的實現。其機制是,如果兩個寄存器不同,右側的晶體管會導通,將相等總線(equality bus)拉低。這個電路重復 30 次,比較所有位:如果有任何不匹配,相等總線會被拉低;如果所有位都匹配,總線保持高電平。左側的三個門實現異或非(XNOR);這個電路可能看起來過于復雜,但它是實現異或非的標準方法。右側的或非門會在時鐘相位 2 之外阻止比較(其重要性將在下文解釋)。
這個電路重復 30 次以比較寄存器
相等總線水平穿過預取器,如果有任何位不匹配,總線就會被拉低。但是什么讓總線保持高電平呢?這由下面的動態電路負責。與常規靜態門不同,動態邏輯由處理器的時鐘信號控制,并依賴電路中的電容來保存數據。386 由兩相時鐘信號控制。在第一個時鐘相位,下方的預充電晶體管導通,將相等總線拉高。在第二個時鐘相位,上方的異或電路被啟用,如果兩個寄存器不匹配,就將相等總線拉低。同時,CMOS 開關在時鐘相位 2 導通,將相等總線的值傳遞給鎖存器。“保持器”(keeper)電路會在相等總線未被顯式拉低時保持其高電平,以避免相等總線上的電壓緩慢消散的風險。保持器使用一個弱晶體管,在總線未被激活時保持其高電平。但如果總線被拉低,保持器晶體管會被 overpowered 并關閉。
這是等值比較的輸出電路,該電路位于預取器右側
這種動態邏輯降低了功耗和電路尺寸。由于總線在相反的時鐘相位充電和放電,避免了通過晶體管的持續電流(相比之下,像 8086 這樣的 NMOS 處理器可能在總線上使用上拉電阻。當總線被拉低時,電流會流過上拉和下拉晶體管,這會增加功耗,使芯片更熱,并限制時鐘速度) 。
增量器
每次預取后,提前指令取指指針必須遞增,以保存下一條要預取指令的地址。遞增這個指針是增量器的工作。(因為每次取指 32 位,所以指針每次遞增 4。但在芯片照片中,你可以看到增量器和限制檢查電路中有一個缺口,最低兩位的電路被省略了。因此,增量器的電路將其值加 1,這樣指針(附加兩個零位)就以 4 為步長遞增。)
構建一個增量器電路很直接,例如,可以使用由 30 個半加器組成的鏈。但問題在于,高速遞增一個 30 位的值很困難,因為進位會從一個位位置傳遞到下一個位位置。這類似于用十進制計算 99999999 + 1;你需要繁瑣地進位,進位,再進位,依此類推,遍歷所有數字,導致這是一個緩慢的順序過程。
增量器采用了一種更快的方法。首先,它幾乎并行地高速計算所有進位。然后,根據進位并行計算每個輸出位—— 如果有進位進入某個位位置,就翻轉該位。
計算進位在概念上很直接:如果值的末尾有一個由 1 組成的塊,所有這些位都會產生進位,但進位會被最右邊的 0 位阻止。例如,遞增二進制數 11011 會得到 11100;最后兩位產生進位,但零位阻止了進位。早在 1959 年,英國曼徹斯特大學就開發出了實現這種功能的電路,被稱為曼徹斯特進位鏈(Manchester carry chain)。
在曼徹斯特進位鏈中,為每個數據位構建一個開關鏈,如下所示。對于 1 位,閉合開關;對于 0 位,打開開關(開關由晶體管實現)。為了計算進位,從右側輸入一個進位信號。信號會通過閉合的開關,直到遇到一個打開的開關,然后被阻斷。進位鏈上的輸出為我們提供了每個位置所需的進位值。
曼徹斯特進位鏈的概念,4 位。
由于曼徹斯特進位鏈中的開關都可以并行設置,且進位信號能高速通過開關,所以這個電路能快速計算出我們需要的進位。然后,進位會并行翻轉相關的位,比直接使用加法器快得多地得到結果。
當然,在實際實現中存在復雜情況。進位鏈中的進位信號是反相的,所以低信號通過進位鏈傳播以表示進位(將信號拉低比拉高更快)。但在必要時,需要讓線路拉高。與相等電路一樣,解決方案是動態邏輯。也就是說,進位線在一個時鐘相位預充電至高電平,然后在第二個時鐘相位進行處理,可能會將線路拉低。
下一個問題是,進位信號在通過多個晶體管和長導線時會減弱。解決方案是每個段都有一個電路來放大信號,使用時鐘控制的反相器和不對稱反相器。重要的是,這個放大器不在進位鏈路徑中,所以不會減慢鏈中信號的速度。
增量器中典型位的曼徹斯特進位鏈電路。
上圖展示了增量器中典型位的曼徹斯特進位鏈電路實現。鏈本身在底部,晶體管開關如前所述。在時鐘相位 1,預充電晶體管將進位鏈的這個段拉至高電平。在時鐘相位 2,鏈上的信號通過右側的 “時鐘控制反相器” 產生本地進位信號。如果有進位,下一個位會被異或門翻轉,產生遞增后的輸出。“保持器 / 放大器” 是一個不對稱反相器,產生強低輸出但弱高輸出。當沒有進位時,其弱輸出使進位鏈保持高電平。但一旦檢測到進位,它會強烈地將進位鏈拉低以增強進位信號。
但這個電路對于期望的性能來說仍然不夠。增量器并行使用了第二種進位技術:進位跳躍(carry skip)。其概念是查看位塊,并允許進位跳過整個塊。下圖展示了進位跳躍電路的簡化實現。每個塊由 3 到 6 位組成。如果一個塊中的所有位都是 1,與門(AND gate)會導通進位跳躍線中的相關晶體管。這允許進位跳躍信號以塊為單位(從左到右)傳播。當它到達一個包含 0 位的塊時,相應的晶體管會關閉,像在曼徹斯特進位鏈中那樣阻止進位。與門都并行工作,所以晶體管會快速并行導通或關閉。然后,進位跳躍信號通過少量晶體管,無需經過任何邏輯(進位跳躍信號就像一列快車,跳過大多數站點,而曼徹斯特進位鏈是到所有站點的慢車)。與曼徹斯特進位鏈一樣,進位跳躍的實現需要在線路上使用預充電電路、保持器 / 放大器和時鐘控制邏輯,但我將跳過細節。
一個抽象且簡化的進位跳躍電路。塊大小與 386 的電路不匹配。
一個有趣的特點是大型與門的布局。一個 6 輸入的與門是一個大型器件,難以適配增量器的一個單元。解決方案是將該門分散在多個單元中。具體來說,該門使用標準的 CMOS 與非門(NAND)電路,NMOS 晶體管串聯,PMOS 晶體管并聯。每個單元有一個 NMOS 晶體管和一個 PMOS 晶體管,鏈在末端連接以形成所需的與非門(對輸出取反得到所需的與功能)。這種分散的布局技術不常見,但能使每個位的電路大致保持相同大小。
由于這些技術,增量器電路的逆向工程頗具挑戰性。特別是,預取器的大部分由一個電路塊重復 32 次組成,每個位對應一次。另一方面,增量器由四個不同的電路塊組成,以不規則模式重復。具體來說,一個塊啟動進位鏈,第二個塊繼續進位鏈,第三個塊結束進位鏈。結束塊之前的塊不同(一個大晶體管驅動最后一個塊),總共有四個變體。這種不規則模式在之前的預取器照片中可見。
對齊網絡
預取器的底部會根據需要旋轉數據以對齊。與某些處理器不同,x86 不強制要求內存訪問對齊。也就是說,一個 32 位的值無需在內存中從 4 字節邊界開始。因此,一個 32 位的值可能會跨預取隊列的兩個 32 位行拆分存儲。此外,當指令解碼器從預取隊列中獲取指令的一個字節時,該字節可能在預取隊列的任何位置。
為了處理這些問題,預取器包含一個對齊網絡,它可以旋轉字節,以輸出符合處理器其他部分所需對齊方式的一個字節、字或四個字節。
下圖展示了這個對齊網絡的一部分。從預取隊列(頂部)輸出的每個位有四條導線,用于 24 位、16 位、8 位或 0 位旋轉。每條旋轉導線連接到 32 條水平位線之一。最后,每條水平位線有一個輸出抽頭,連接到下方的數據通路。(垂直線在芯片的下層 M1 金屬層,而水平線在上層 M2 金屬層。為了拍攝這張照片,我移除了 M2 層以顯示下層。原始水平線的陰影仍然可見。 )
對齊網絡的一部分。
其原理是,通過選擇一組垂直旋轉線,預取隊列的 32 位輸出將向左旋轉相應的量。例如,要旋轉 8 位,位會通過 “旋轉 8” 線向下傳輸。預取隊列的位 0 會激活水平位線 8,位 1 會激活水平位線 9,依此類推,位 31 會回繞到水平位線 7。由于水平位線 8 連接到輸出 8,結果就是位 0 作為位 8 輸出,位 1 作為位 9 輸出,依此類推。
對齊 32 位值的四種可能性。上方的四個字節按指定方式移位,以產生下方所需的輸出。
對于對齊過程,預取隊列中一個 32 位輸出可能會以四種不同方式跨兩個 32 位條目拆分存儲,如上所示。這些組合由多路復用器(multiplexers)和驅動器(drivers)實現。兩個 32 位多路復用器選擇預取隊列中相關的兩行(上方的藍色和綠色)。四個 32 位驅動器連接到四組垂直線,其中一組驅動器被激活以產生所需的移位。每個驅動器的每個字節都進行了布線,以實現上述對齊。例如,旋轉 8 位的驅動器從 “綠色” 多路復用器獲取其最高字節,從 “藍色” 多路復用器獲取其他三個字節。結果是,跨兩個隊列行拆分的四個字節被旋轉,形成一個對齊的 32 位值。
符號擴展
最后一個電路是符號擴展電路。假設你想將一個 8 位數值與一個 32 位數值相加。一個無符號 8 位數值可以通過簡單地將高位填充為零來擴展為 32 位。但對于有符號數值來說,情況就更復雜了。例如,-1 的 8 位表示是 0xFF,而其 32 位表示是 0xFFFFFFFF。要將一個 8 位有符號數值轉換為 32 位,高 24 位必須用原始數值的最高位(即符號位)來填充。換句話說,對于正數,擴展位填充為 0;而對于負數,擴展位填充為 1。這個過程就稱為符號擴展。9
在 386 處理器中,預取器底部的一個電路負責對指令中的數值進行符號擴展。該電路支持將 8 位數值擴展為 16 位或 32 位,也支持將 16 位數值擴展為 32 位。此電路會根據指令的要求,用零或符號位來擴展數值。
下圖展示了這個符號擴展電路的一個位的結構。它由左側和右側的鎖存器以及中間的多路復用器組成。鎖存器是采用標準的 386 電路,通過 CMOS 開關構建而成(參見腳注)。7 多路復用器從三個值中選擇一個:來自交換網絡的位值、用于符號擴展的 0,或用于符號擴展的 1。如果選擇位值,多路復用器由 CMOS 開關構成;若選擇 0 或 1 值,則由兩個晶體管構成。這個電路被復制了 32 次,但最低字節只包含鎖存器,沒有多路復用器,因為符號擴展不會修改最低字節。
預取器中與 31-8 位相關的符號擴展電路。
符號擴展電路的第二部分用于確定這些位應該填充 0 還是 1,并將控制信號發送給上面的電路。左側的門電路確定符號擴展位應該是 0 還是 1。對于 16 位符號擴展,該位來自數據的第 15 位;而對于 8 位符號擴展,該位來自第 7 位。右側的四個門電路生成用于每個位的符號擴展信號,分別為 31-16 位范圍和 15-8 位范圍產生獨立的信號。
該電路確定哪些位應該填充 0 或 1。
這個電路在芯片上的布局有些不同尋常。預取器的大部分電路由 32 個相同的列組成,每列對應一個位。8 上面描述的電路只實現了一次,使用了大約 16 個門電路(未顯示緩沖器和反相器)。盡管如此,該電路被擠在第 17 位到第 7 位的位置,導致布局出現了不規則性。此外,與 386 處理器的其他部分相比,該電路在硅片上的實現方式也很特別。386 的大部分電路使用兩層金屬進行互連,盡量減少多晶硅布線的使用。然而,上面提到的這個電路還使用了長段的多晶硅來連接各個門電路。
符號擴展電路的布局。該電路位于預取隊列的底部。
指令在芯片中的傳輸路徑
指令在 386 芯片中的傳輸路徑曲折復雜。首先,右上角的總線接口單元從內存中讀取指令,并通過 32 位總線(藍色)將其發送到預取單元。預取單元將指令存儲在 16 字節的預取隊列中。
指令在預取隊列中進進出出,路徑曲折。
如何從預取隊列中執行一條指令呢?事實證明,這有兩條不同的路徑。假設你要執行一條將 12345678 加到 EAX 寄存器的指令。預取隊列中會存放五個字節:05(操作碼)、78、56、34 和 12。預取隊列通過 8 位總線(紅色)將操作碼逐個字節地提供給解碼器。該總線從預取隊列的對齊網絡中取出最低 8 位,并將該字節發送到一個緩沖器(紅色箭頭頭部的小方塊)。操作碼再從這里傳輸到指令解碼器。10 指令解碼器反過來使用大型查找表(PLA)將 x86 指令轉換為包含 19 個不同字段的 111 位內部格式。
另一方面,指令的數據字節通過 32 位數據總線(橙色)從預取隊列傳輸到算術邏輯單元(ALU)。與前面提到的總線不同,這條數據總線分布較廣,每條線貫穿數據通路的每一列。因此,數據也可以存儲到寄存器中。例如,MOV(移動)指令可以將指令中的一個值(即 “立即數”)存儲到寄存器中。
結論
386 處理器的預取隊列包含約 7400 個晶體管,比整個英特爾 8080 處理器的晶體管數量還多(而且這還只是隊列本身,不包括預取控制邏輯)。這體現了處理器技術的飛速發展:386 中一個功能單元的一部分所包含的晶體管數量,就超過了 11 年前的整個 8080 處理器。而這個單元還不到整個 386 處理器的 3%。
每次觀察 x86 電路時,我都能看到為支持向后兼容所付出的復雜性,也更能理解為什么精簡指令集計算機(RISC)會流行起來。預取器也不例外。其復雜性在很大程度上源于 386 對未對齊內存訪問的支持,這需要一個字節移位網絡來將字節調整到 32 位對齊狀態。此外,在指令總線的另一端是復雜的指令解碼器,用于解碼復雜的 x86 指令。而解碼 RISC 指令則要簡單得多。
無論如何,希望你能對預取電路的介紹感興趣。我計劃撰寫更多關于 386 的內容,所以請在 Bluesky(@righto.com)上關注我,或通過 RSS 獲取更新。我之前已經寫過多篇關于 386 的文章,一個不錯的起點可能是我對 386 芯片的綜述。
原文:https://www.righto.com/
歡迎加入EETOP RISC-V等微信群
邀請報名
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.