線程並行
當代的軟件趨向於運行在多線程模式,或者是並行處理。網絡包傳輸和web服務可以運行以多線程的方式運行以達到更高的性能。甚至是桌面應用也開始增加並行性。Intel架構師已經開始嘗試利用TLP( thread-level parallelism) 線程並行性,用來在較少的晶體管和功耗下就能達到更高的性能。
高端和終端的服務器市場,多核已經被廣泛的使用,已獲得更高的性能。通過增加處理器,應用可以通過在更多的處理器上運行更多的線程已達到更高的性能。這些線程可能屬於同一個應用,也可以屬於不同的應用。多核系統已經被使用多年,高端程序員已經可以熟練的利用多核來達到更高的性能。
近幾年,大家討論了一些提高TLP的技術,也提出了產品。比如CMP(chip multiprocessing),兩個處理器被放到了一個die上。這兩個處理器都各自擁有完全的組件和架構資源。處理器之間可以共享更大的片上cache。 CMP和傳統的多核系統基本無關,你可以在傳統的多核系統上使用CMP芯片。然而,CMP chip 將會比單核chip面積上大的多,因此造價更高。並且這也沒有解決die的面積和功耗問題。
另一種方法就是通過線程切換,允許單核去執行多線程。通過時間片切換,實現多線程切換。然而這將導致多餘的執行,但是可以有效地減少訪問memory導致的latency。如果多線程基於event切換,那麼當多線程遇到cache miss的情況時,可以有效地減少latency。這個方法對於大量cache miss 和執行類似任務的線程很有效。但是時間片或者是基於event切換都沒有實現提高資源的利用效率,比如分支預測失敗和指令依賴。
最終,可以用過單核,不切換的方式實現多線程。線程可以同時執行,以更高效的利用資源。這個方法可以最大資源的利用效率。在單位晶體管和功耗時,實現了性能的最大化。
Hyper-Threading 技術爲intel 架構提出了多線程的方法。這篇文章我們將要討論在Xeon 處理器家族中,Hyper-Threading技術的首次應用。
Hyper-Threading技術架構
Hyper-Threading 在單個物理處理器上實現了多個邏輯處理器。我們爲每一個邏輯處理器都做了一份architecture 狀態的拷貝,邏輯處理器之間共享物理執行資源。從軟件到架構的角度,這意味着操作系統或者是用戶的程序可以像在多核處理器上調度多線程一樣,在單核上調度多線程。從微架構的角度,這意味着邏輯處理器將會在共享的執行資源上執行不同線程的指令。
圖2展示了多核系統上,兩個沒有采用hyper-threading技術的物理核心。圖3展示了採用了hyper-threading技術的多核系統。通過在每個物理核心上實現兩份邏輯核心狀態的備份,系統就看起來有四個邏輯核心。
Hyper-Threading 技術首次實現在Xeon家族的雙處理器和多處理器服務器上。通過有效地利用更多的處理器資源,Xeon 處理器產品可以有效地提高性能。Hyper-Threading技術增加了小於5%的芯片面積和功耗,但是提供了更多的性能。
每一個邏輯處理器都維持一份完整的架構狀態。架構狀態包括寄存器(通用寄存器,控制寄存器和中斷控制寄存器以及狀態寄存器)。從軟件的角度看,一旦架構狀態有多份,那麼處理器可以虛擬的看成兩個處理器。存儲架構狀態的寄存器的晶體管的數量是非常小的。邏輯處理器之間基本上共享了所有的資源,比如cache,執行單元,分支預測,控制邏輯以及總線。
每個邏輯處理器都擁有自己的中斷控制器,因此中斷可以發送給指定的邏輯處理器,並且準確的被相應的處理器處理。
Hyper-threading 技術在Intel Xeon 產品中的首次應用
在Xeon產品中實現Hyper-Threading技術時,預先設定了幾個目標。其中一個是在實現Hyper-Threading的同時最小化die的面積。因爲邏輯處理器共享了主要的微架構資源,僅僅有少部分結構被複制,因此實現該技術的面積佔用了全die面積不到5%。
第二個目標是當一個邏輯處理器阻塞時,另一個邏輯處理器可以繼續工作。一個邏輯處理器可能會因爲cache miss,處理branch預測失敗和等待依賴的指令執行完畢而暫時的阻塞一段時間。獨立的前向工作流應當保證在兩個線程同時執行時,沒有邏輯處理器可以佔用全部的資源。這是通過劃分或者限制每個thread可以擁有的資源數量實現的。
第三個目標是單線程軟件在hyper-threading處理器上運行時應當擁有和普通處理器一樣的速度。這意味着,如果單線程運行在處理器上,那麼它將擁有全部的資源。從更高的角度看,微架構的pipeline如figute4所示,其中緩衝隊列將pipeline分割。Queue要麼被對半分,要麼被複制,以保證每個線程都可以獨立的前行。
接下來的章節我們將會梳理pipeline,討論一下主要的功能,並且闡述資源的共享與複製。
Frond End 前端
流水線的前端負責將指令傳輸給後續的pipe 階段。如圖5-a所示,指令最初從Execution Trace Cache(TC)或者L1 指令cache中獲得。圖5-b表明只有TC miss的時候,cpu才從L2 Cache中取指,譯碼。靠近TC的是微指令ROM(Microcode ROM),存儲爲較長,較複雜的IA-32指令存儲譯碼過的微碼。
Execution Trace Cache (TC)
TC存儲譯碼過的指令,也稱作微指令。大多數程序中的指令都是從TC中取出並執行的。獨立的兩組指令PC用於追蹤各自的線程的執行。兩個邏輯處理器可以每週期仲裁一次,以獲得TC的所有權。如果兩個邏輯處理器在同一時刻擁有權限,那麼將會仲裁給一個,在下個週期仲裁給另一個。比如,如果兩個邏輯處理器都請求trace cache的權限,第一個週期爲一個邏輯處理器取指令,那麼下一個週期則爲另一個處理器作用。如果一個邏輯處理器阻塞了,不能夠使用TC,那麼另一個邏輯處理器將可以每個週期都全帶寬的訪問TC。
TC Entry被線程信息所標記,並且可以按照需要動態的分配。TC結構爲8路組相連,使用LRU替換算法。共享的特性可以使一個邏輯處理器在需要的情況下,擁有比另一個更多的Entry。
Microcode ROM
當遇到了複雜的指令時,TC發送一個微指令指針給Microcode ROM。Microcode ROM將會控制取微碼,然後將控制權返回給YC。兩個邏輯處理器擁有各自獨立的微指令指針。
邏輯處理器也共享Microcode ROM。訪問Microcode ROM的權限在兩個邏輯處理器之間切換。
ITLB 和 分支預測
如果TC miss,將會從L2 Cache中取指,解碼爲微碼後,存入TC中。ITLB接收到來自TC對指令的請求,ITLB將下一條指令的地址(PC)翻譯爲物理地址。請求被髮送給L2 Cache,並得到來自L2 Cache的響應。這些指令將會存放在streaming buffer,直到他們被譯碼。
在hyper-threading中,ITLB被複制了兩份。每個邏輯處理器有它自己的的ITLB,而且都有自己得一套指令指針,以追蹤程序的PC。取指邏輯負責向L2 Cache發送請求,它會基於先來先處理的準則,對取值邏輯進行仲裁。一般會爲兩個邏輯處理器至少處理一個請求。因此,兩個邏輯處理器可以同時有pending的取指。
每個邏輯處理器都有一套64-byte的stream buffer,爲指令解碼階段緩存指令。ITLB和streaming buffer都是較小的結構,因此這個結構的複製對die的面積佔用較小。
分支預測結構可以是複製的或者共享的。用於預測返回指令地址的Return stack buffer也被複制了,因爲這是一個非常小的部件,並且call/return pair的預測根據獨立的線程進行預測會更加準確。BHB(分支歷史緩存)用於在global history array中爲每個邏輯處理器查找全局歷史。然而,因爲global history array很大,因此這是一個共享結構,其內部項會被邏輯處理器ID號打上tag。
IA-32 指令譯碼
IA-32 指令是譯碼起來是非常笨重的,因爲指令是變長的,並且有不同的選擇。譯碼將會花費大量的邏輯和臨時狀態。幸運的是,TC 會命中大部分的uop,因此我們只需要在TC miss的時候譯碼。
譯碼邏輯從stream buffer中取出指令,將其譯碼爲uop。當兩個線程同時譯碼時,stream buffer會在線程之間切換,因此線程會共享譯碼邏輯。譯碼邏輯需要爲兩個線程保存中間狀態,即使它只爲一個邏輯寄存器譯碼。總體上說,在切換到另一個邏輯處理器之前,會有另一個邏輯處理器的幾條指令被譯碼。我們會基於die的大小以及複雜性來確定邏輯處理器切換的顆粒度。當然,如果只有一個邏輯處理器需要譯碼,那麼它會佔據全部的譯碼帶寬。譯碼指令將會被寫入TC,並forward到uop隊列中。
Uop隊列
Uop來自以下三個來源:
- Trace cache
- Microcode ROM
- 譯碼邏輯
Uop會被放置到uop queue中。這個queue將front-end即前端和亂序執行執行單元解耦。Uop queue根據邏輯處理器分爲兩半。切換使得邏輯處理器可以有獨立的forward邏輯,不會因爲另一個邏輯處理器的TC miss或者是執行單元的暫停而阻塞。
亂序執行單元
亂序執行單元由分配、寄存器重命名、調度和執行單元組成,如圖6所示。這部分亂序的執行指令。一旦他們的輸入準備好,就立刻執行,而不考慮原本的程序中的指令順序。
分配
亂序執行單元有緩衝buffer用於執行重排序,跟蹤和順序操作。分配器從uop queue中取出uop,然後分配執行uop所需要的buffer。緩衝buffer包括 126項重排序buffer, 128個整數和128個浮點物理寄存器,48個load 和 24個store 項buffer。我們拆分了其中的幾個重要buffer,所以每個邏輯處理器可以最多使用其中的一半數目。比如,邏輯處理器最多可以使用63個re-order buffer,24個load buffer和12個store buffer。
如果兩個邏輯處理器在uop queue都有uop要處理,分配器將會每個週期都爲邏輯處理器切換。如果邏輯處理器已經使用了它所分配的資源的上限,那麼分配器將會向這個邏輯處理器發送一個暫停信號,而後爲另一個邏輯處理器分配資源。如果uop queue中只包含一個邏輯處理器的uops,那麼分配器將會每個週期都爲這個邏輯處理器繼續分配資源,以優化分配帶寬,儘管還需要滿足資源上限條件。
通過限制關鍵buffer的資源佔用限制,可以公平的對待每個邏輯處理器並且避免死鎖。
寄存器重命名
寄存器重命名將IA-32寄存器命名爲物理寄存器。這將會使8個通用IA-32整數寄存器變爲動態可調節的128個物理寄存器。重命名邏輯通過寄存器重命名錶格(RAT)以追蹤最新的架構寄存器。這樣新來的指令就可以明確其操作數架構寄存器對應的物理寄存器。
因爲邏輯處理器需要維護並追蹤自己的完整的架構狀態,因此我們爲每一個邏輯處理器都設置了一個RAT。重命名過程與上述的分配邏輯並行執行。因此分配器和寄存器重命名會同時處理相同的uops。
一旦uops已經完成了分配和寄存器重命名,他們就會被分配到兩組queue中,一組作用域寄存器操作(load and store),另一組負責所有的其他操作。這兩組queue分別叫做存儲指令隊列和通用指令隊列。這兩組隊列也被切分,每個邏輯寄存器最多使用一半。
指令調度
指令調度是亂序執行的關鍵所在。五個uop調度器用於在不同的執行單元之間調度不同類型的指令。每週期可以最多派發6條uops。如果uops的輸入操作數準備好了,並且執行單元已經空出,可以使用,那麼調度器將會選擇哪個uops可以開始執行。
存儲指令隊列和通用指令隊列會盡快的將uops發送到五個調度隊列。如果需要的話,會每週期爲邏輯處理器切換操作。
每個調度器都有自己的8項到12項的調度隊列。調度器從調度隊列中選擇uops,發送給執行單元。調度器不會基於uops究竟屬於哪一個邏輯處理器來選擇uops。也就是說,調度器無視邏輯處理器。Uops可以僅僅根據輸入操作數和執行單元而被選擇。舉個例子,調度器可以同時分配兩個邏輯處理器額各自兩條uops。爲了避免死鎖,並且保證公平,我們隊邏輯處理器在調度隊列中的數目上限進行了設置。這個上限值取決於調度隊列的大小。
執行單元
執行單元和存儲層次也不會區分不同的邏輯處理器。因爲源寄存器和目標寄存器已經在同一個物理寄存器池中被重命名,因此uops僅通過讀取物理寄存器文件可以知道其目標寄存器對應的物理寄存器。通過比較物理寄存器的編號就可以將操作結果forward給其他的uops,而不需要明確邏輯寄存器號。在執行之後,uops被存放到重排序緩衝中。重排序緩衝將執行階段和retire階段解耦。重排序緩衝爲每個邏輯處理器各分一半。
退休單元
Uop退休邏輯將會按照程序的順序改變架構狀態。退休邏輯會追蹤兩個邏輯處理器中的待退休指令。然後通過在兩個邏輯處理器之間切換,並按照程序的順序提交、改變架構狀態。退休邏輯會爲一個邏輯處理器退休uop,然後切換到另一個。如果一個邏輯處理器一直沒有準備好退休uop,那麼退休單元會全力退休另一個邏輯處理器。
一旦store退休,store的數據需要被寫入第一級data cache。選擇邏輯會在邏輯處理器之間切換,以將數據存儲到cache中。
Memory 子系統
Memory子系統包括DTLB,L1 data cache , L2 data cache 和L3 data Cache(L3 cache只在Xeon處理器中有)。對memory子系統的訪問也不考慮邏輯處理。存儲子系統只是來了請求,就處理。
DTLB
DTLB將邏輯地址翻譯爲物理地址。它是64路全相連。每一項可以映射4K或者4M page大小。儘管DTLB是兩個邏輯處理器中共享的結構,但是每一項都會包含一個處理器ID。每個邏輯處理器都有自己的一個保留寄存器以保證公平性和在DTLB miss時,進行forward。
L1 Data Cache, L2 Cache,L3 Cache
L1 data cache是4路組相連,每個cacheline 64 byte。它是寫直達cache,這意味着寫操作也會直接寫入L2 Cache。L1 Cache 是虛擬地址索引,物理地址做tag。
L2 和 L3 Cache是8路組相連,每個cacheline 128 byte。L2 和L3 cache是物理地址索引。邏輯處理器之間共享cache。
因爲邏輯處理器共享cache,因此可能有潛在的cache衝突,這可能會導致更低的性能。但是也可能會存在共享cache中的數據。比如,一個邏輯處理器可能預取另一個處理器需要的指令或者數據。這在服務器應用程序中很常見。在生產者-消費者模型中,一個邏輯處理器可能產生另一個邏輯處理器需要使用的數據。在這類case中,可以挖掘潛在的更高的性能。
BUS
如果cache miss,那麼邏輯處理器將會訪問bus logic。Bus logic包括本地的APIC中斷控制器,也包括片外的系統存儲和I/O 空間。總線邏輯也處理來自外部總線對cacheable 地址的snoop請求和本地APIC輸入的終端。
從服務的角度來看,邏輯處理器的請求是根據先來先處理的原則。Queue和Buffer空間是共享的。我們不會給其中的一個邏輯處理器優先權限。
我們也會區分來自不同邏輯處理器的請求。對本地APIC的請求和中斷資源是唯一的,並區分不同的邏輯處理器。總線邏輯也會基於邏輯處理器來處理barrier fence(屏障操作)和memory order操作。
對於debug功能,邏輯處理器的ID號被顯示的發送給處理器的外部總線。其他的總線操作,如cache line evict和預取操作,會根據產生transaction的請求來獲取邏輯處理器的ID號。
單任務和多任務模式
當一個軟件線程要執行時,爲了優化性能,提供了兩種模型多任務模型(multi-task MT)和單任務模型(ST)。在MT模式中,有兩個工作的邏輯處理器,其中的一些資源被如前所述的方式切分。ST模式分爲兩種,single-task 邏輯處理器0(ST0)和single-task 邏輯處理器1(ST1)。在ST-0或者ST-1模式中,只有一個邏輯處理器處於工作狀態。在MT模式中被切分的資源被單個處理器暢享。IA-32 Intel 架構有一個指令叫做HALT,將會停止處理器的執行,然後使得處理器進入低功耗狀態。HALT是一條特權指令,這意味着只有操作系統或者其他的ring-0級別的程序可以執行這條指令。用戶級別的程序是不可以執行HALT執行的。
如果處理器採用了Hyper-Threading技術,執行HALT指令,處理器將會從MT模式切換到ST-0或者ST-1模式。比如,如果邏輯處理器0執行HALT,那麼只有邏輯處理器1處於工作狀態。那麼物理寄存器將會處於ST-1模式。先前被切分的資源將會被邏輯處理器1全部助戰。如果邏輯處理器1也執行了HALT操作,那麼物理處理器可以進入低功耗狀態。
在ST0或者ST1模式中,中斷髮送給已經HALT的處理器將會導致處理器進入MT模式,操作系統負責管理MT模式的切換。
圖7總結了上述的討論。在ST0或者ST1模式資源被完全分配給單個邏輯處理器。在MT模式,資源在兩個邏輯處理器間共享。
操作系統和應用程序
使用了Hyper-Threading 技術的處理器在操作系統和應用程序看來,處理器核心數翻倍。操作系統將會當把邏輯處理器當做物理處理器進行管理,調度程序和線程。然而,爲了達到最高的性能,操作系統會實現兩種優化。
第一種是如果僅有一個邏輯處理器active使用HALT指令。使用HALT將會使處理器進入ST0或者ST1模式。不使用這個優化的操作系統將會在一個邏輯處理器idle,另一個邏輯處理器active的處理器上工作。並且idle的邏輯處理器會持續的判斷是否有人物要執行,這個叫做idle loop。Idle loop將消耗執行資源,如果這些執行資源釋放,那麼性能將會更高。
另一種優化就是將軟件線程調度到邏輯處理器上。總體上來說,爲了更好的性能,操作系統將會在調度線程到相同的物理處理器之前,調度線程到不同的物理處理器上。這個優化將會使得線程儘可能的執行在不同的物理核心上。
性能提升
在單核處理器和多核處理器上,Hyper-threading技術可以提升21%的性能。
在網站服務器的workload中,可以獲得16%到28%的性能提升。
筆記:
Sub System |
IP Block |
Share |
Duplicate |
Partion |
前端 |
Trace Cache |
√ |
|
|
Microcode ROM |
√ |
|
|
|
ITLB |
|
√ |
|
|
分支預測 |
|
√ |
|
|
Return stack |
|
√ |
|
|
譯碼 |
√ |
|
|
|
Uop 隊列 |
|
|
√ |
|
亂序執行 |
分配 |
|
|
√ |
寄存器重命名 |
√ |
|
|
|
指令調度 |
√ |
|
|
|
執行單元 |
√ |
|
|
|
重排序緩衝區 |
|
|
√ |
|
存儲系統 |
DTLB |
√ |
|
|
Cache |
√ |
|
|
歡迎關注我的公衆號《處理器與AI芯片》