本文介紹 Armv8-A 架構的內存序模型,并介紹 arm 的各種內存屏障。本文還會指出一些需要明確內存保序的場景,并指明如何使用內存屏障以讓程序運行正確。
本文檔適用于底層代碼(比如 boot 代碼或驅動)開發者,以及共享內存的多線程應用程序開發者。
3. 前置知識
譯者:第 3 章內容屬于《Learn the architecture - AArch64 memory model》一文,是本文所要探討內容的前導知識。
3.1 Memory types
系統中所有未標記為 faulting 的地址都會被賦予一個 memory type。memory type 用來從 high level 角度描述處理器與地址區域的交互行為。Armv8-A 和 Armv9-A 架構下有兩種 memory types:Normal memory 和 Device memory。
注意:Armv6 和 Armv7 下還有第三種 memory type:Strongly Ordered。在 Armv8 下,該類型對應 Device-nGnRnE。
3.2 Normal memory
Normal memory 用于行為看起來像是一個 memory 的東東,包括 RAM、Flash 或 ROM。代碼只能位于被標記為 Normal 的位置。
Normal 是系統中最常見的 memory type,如下圖:
3.2.1 內存訪問序
通常情況下,處理器會按照程序所指定的順序運行指令。一個指令會按照程序所指定的次數運行,并且每次只運行一個指令,這稱為 "Simple Sequential Execution(SSE)" 模型。大多數現代處理器都似乎遵循此模型,但實際上底層會進行一系列優化,以幫助提升性能。
對一個被標記為 Normal 的內存地址進行訪問是不會產生直接副作用的(direct side-effects)。也就是說,對此內存地址進行讀取會返回數據,且不會引起數據發生變化,或直接觸發一些其他的行為。正因為如此,處理器可以對“對 Normal 類型內存地址的訪問”進行訪問合并、投機讀(譯者:speculative access,我覺得譯為“預讀”問題也不是很大)或是亂序讀。
3.3 Device memory
Device memory type 是用來描述外設的。外設寄存器通常稱為 Memory-Mapped I/O(MMIO)。下圖是一個示例地址映射下被標記為 Device 的內存區域:
對 Normal type 內存的訪問是沒有副作用的,而對 Device type 內存的訪問則相反。Device memory type 用于有訪問副作用的內存地址。
舉例來說,對一個 FIFO 的訪問通常會導致其移動到下一個數據片段。這意味著對 FIFO 的訪問次數其影響至關重要,因此處理器必須嚴格遵循程序的定義。
Device 區域不是 cacheable 的,這是因為大概率你應該是不會想對設備訪問進行緩存的。
Device type 的內存區域上不允許做數據的投機讀。處理器只能訪問 architecturally accessed 的內存,所謂的 architecturally accessed,意思就是指令在執行時所明確要訪問的內存。
譯者:這里值得重點注解一下,本文行文中有多種對內存訪問的定語修飾,比如 architecturally accesses、explicit data accesses,其意思都差不多,指的是一條指令中明確的對內存所進行的訪問,典型如“LDR X0, [X1]、STR X0, [X1]”這種。那難道還有非 architecturally accesses 或 implicit data accesses 嗎?有的,比如一次 load 背后可能會涉及到頁表查詢,頁表查詢也是一種內存訪問,但其并不是由指令顯式所指定的,頁表查詢這類所引發的內存訪問,不是 architecturally accesses 或 explicit data accesses。
不應該把指令放在 Device 區域。推薦的做法是總是將 Device 區域標記為不可執行,否則處理器可能會從該區域做指令預取,進而會在 FIFOs 這類“讀敏感”設備上搞出問題。
注意:這里有一個容易被忽略的微妙區別。將一個區域標記為 Device,只會阻止對其進行數據的投機讀。將一個區域標記為 non-executable,會阻止指令預取。這意味著,如果要阻止對一個區域的一切投機訪問,需要將其同時標記為 Device 和 non-executable。
3.3.1 Device type 的 sub-types(子類型)
Device type 有四種子類型,分別對應不同的限制級別。以下是最寬松的幾種子類型:
-
Device-GRE
-
Device-nGRE
-
Device-nGnRE
該子類型是最嚴格的:
-
Device-nGnRnE
Device 后面的字母表達的是屬性的組合:
-
Gathering(G, nG)。表示訪問可以被合并(G)或不可以被合并(nG)。意思是可能會將對同一地址的多個訪問合并為一個訪問,或將多個小的訪問合并為一個大的訪問。
-
Re-ordering(R, nR)。表示對同一外設的訪問可以被亂序(R)或不可以被亂序(nR)(譯者:我覺得翻譯成“亂序”或“重排”都行)。當允許亂序時,其亂序規則與 Normal type 一致。
-
Early Write Acknowledgement(E, nE)。此屬性決定一個 write 操作何時可以被認為是“已完成”的。如果允許 Early Write Acknowledge(E)(譯者:early 可以簡單理解為“提前”,在事實生效之前,具體討論見“9. 一次訪問何時被認為是“已完成””),則一旦一個 write 操作對其他觀察者可見,即使該訪問并未真正到達其目的地,該訪問依然會被視為“已完成”。舉例來說,一個 write 操作只需要到達 interconnect 中的 write buffer,即可對其他 Processing Elements(PEs,譯者:就是一個處理器啦)可見。如果不允許 Early Acknowledge(nE),則寫操作必須到達其目的地。
下面是兩個例子:
- Device-GRE。此允許 gathering、re-ordering 以及 early write acknowledgement。
- Device-nGnRnE。不允許 gathering、re-ordering 以及 early write acknowledgement。
上面已經講過 re-ordering 的工作原理,但尚未涉及 gathering 或 early write acknowledgement。gathering 可以將對同一地址的多個內存訪問合并為一個 bus transaction,如此實現對訪問的優化。early write acknowledgement 表示是否允許內存系統在一個 buffer 達到 core 和外設之間的 bus 上時,就發出 write acknowledgement,這樣即使外設尚未收到此 write 操作,其他 PEs 也可以觀測到此 write 操作。
注意:Normal Non-cacheable 以及 Device-GRE 看起來是一回事,實則不是。Normal Non-cacheable 允許數據的投機訪問,而 Device-GRE 不然。
3.3.2 處理器真的會在不同 type 上有不同行為?
memory type 描述了一個地址的可允許行為(譯者:“行為”指合并、亂序、投機讀等)。咱們只關注 Device type,下圖展示了所允許的行為:
可以看到,Device-nGnRnE 是最嚴格的子類型,可允許的行為是最少的。Device-GRE 是最不嚴格的,因此其所允許的行為也是最多的。
值得注意的是,Device-nGnRnE 所允許的行為也是 Device-GRE 所允許的。舉例來說,對 Device-GRE 內存并不要求一定要使用 gathering —— 它只是允許 gathering。因此處理器是可以將 Device-GRE 當作 Device-nGnRnE 來對待的。
這個例子很極端,在 Arm Cortex-A 處理器上似乎并不會這樣。然而,處理器通常不會在所有 type 以及 sub-type 間做區別對待(譯者:理解為處理器的設計也不可能做的如 spec 描述的那么細),比如對 Device-GRE 和 Device-nGRE 用同一種方式處理。這只在 type 或 sub-type 總是更嚴格時會這樣。
有些 interconnects 并不能完全支持 DEvice-nGnRnE 的要求。舉例來說,一個對 PCIe Base Register(BAR) 空間的 Device-nGnRnE write,一旦在其到達 PCIe topology 之后就立刻變成一個 posted write(一個無需“寫完成”response 的 write)。此場景下,該 write 訪問只會有 Device-nGnRE 屬性,因為目標 endpoint 無法提供 write 的 response(譯者注:目的端都壓根不能回復 response 了,就不能強行要求 nE),而是由某些中間組件(比如 PCIe Root Port)來提供。然而,對 PCIe 配置空間的 Device-nGnRnE write 是一個 non-posted write(需要“寫完成”response 的 write),因此這些類型訪問的 Device-nGnRnE 需求是可以被滿足的。
4. 內存序(memory ordering)
Armv8-A 是個弱內存序體系結構,其所支持的內存訪問,不會強加任何需要被發起或觀察的依賴關系(譯者:This architecture permits memory accesses which impose no dependencies to be issued or observe),并且會以與 program order 所指定順序完全不同的順序完成。
這種弱內存序的內存行為,只會在以下場景下被允許:
- 所指向內存是 Normal、Device-nGRE 或 DeviceGRE,或
- 跨越一個外設的 Device-nR 訪問。
內存亂序可以讓處理器運行地更快,如下圖所示:
圖 4 中,有三條 program order 指令:
- 第一條指令,Access 1,對外部內存的 write 進入 write buffer。此指令后面是兩條 program order 的 read。
- 第一個 read,Access 2,未命中 cache。
- 第二個 read,Access 3,命中 cache。
這兩個 read 都可以在 Access 1 的 write buffer 完成 write 之前完成。支持 Hit-Under-Miss 的緩存系統,支持命中 cache 的 load 操作(比如 Access 3),可以在程序中更早的未命中 cache 的 load 操作(比如 Access 2)之前完成。(譯者:這很 make sense,命中 cache 的 load 與未命中 cache 的 load,二者之間并無數據上的沖突,亂序是安全的,有點類似《Intel SDM 之 Memory Ordering》"3. Intel Pentium 及 Intel 486 處理器上的內存序[8.2.1]" 中的 白嫖亂序 )。
4.1 亂序的限制
在 Normal、Device-nGRE 或 Device GRE 內存上的亂序是可能的。考慮下面的代碼序列:
如果處理器對這些訪問進行亂序,可能會導致內存中出現一個錯誤值,而這是不允許的。
對同一內存區域的訪問(譯者:比如本例子中的三條指令,是對 0x1000 - 0x1003 這一相同區域進行訪問),必須在它們之間保序。處理器必須檢測“寫后讀(read-after-write)”冒險(hazard,譯者:“數據冒險”,這是一個標準翻譯),并要保證訪問之間的順序必須是正確的,否則會出現非預期結果。
但這并不意味本示例中的訪問是無法被優化的。處理器可以將兩個 store 合并在一起,最終向內存系統呈現為一個合并后的 store。處理器還要能檢測 load 操作的目標內存是 store 指令所寫目標內存的情況,也就是說,處理器可以直接返回新的值而無需重新從內存中讀取(譯者:類似 store buffer forwarding,參閱《Intel SDM 之 Memory Ordering》“5.5 允許處理器內部的 forwarding[8.2.3.5]”)。
注意:上面的代碼序列是刻意構造以展示數據冒險的。具體實踐中,數據冒險可能不會這么顯而易見。
再舉一個因為存在地址依賴(Address Dependencies)而必須按序執行的例子。地址依賴的一個具體場景是,一個 load 或 store 使用前面的一個 load 的結果作為地址。下面是例子:
LDR X0, [X1]
STR X2, [X0] ; Result of previous load is the address in this store.
下面是另一個例子:
LDR X0, [X1]
STR X2, [X5, X0] ; Result of previous load is used to calculate the address.
如果兩個內存訪問之間存在地址依賴,則處理器會按其 program order 執行。
該規則并不適用于控制依賴(control dependencies),也就是前一個 load 的結果是用來做判斷的(譯者:而不是用來訪問的,比如示例代碼中的 CBZ)。比如:
LDR X0, [X1]
CBZ X0, somewhere_else
LDR X2, [X5] ; The control dependency on X0 does not guarantee ordering.
有些情況下,需要對 Normal 內存的訪問之間或對 Normal 和 Device 內存的訪問之間進行保序,這時候就需要使用屏障指令。
5. 內存屏障(memory barriers)
內存屏障是一類指令的總稱,該類指令可以顯式指定某種形式的保序、同步或對內存訪問的限制。
Armv8 體系結構所支持的內存屏障提供了很多功能,包括:
- load 和 store 指令間的保序。
- load 和 store 指令的完成(completion)。
- 上下文(context)同步。
- 對投機訪問的限制。
有些場景下弱內存序的體系結構亂序行為是個攪屎棍,其會導致非預期結果。本文介紹體系結構所支持的各種類型的內存屏障,并指出一些需要明確保序的典型場景,同時指出如何通過內存屏障來得到預期結果。
6. 啥是觀察者(Observer)?
Armv8-A Architecture Reference Manual 使用“觀察者”(譯者:因為 observe 既是 Observer 的詞根,也會當動詞來用,為行文清晰起見,“觀察者”后續一律不翻譯而是直接用 Observer)這一術語來描述內存屏障所能產生的影響。
一個 Observer,指的是一個 Processor Element(PE),或系統中的其他部件,這些部件可以從內存中 read,或向內存中 write,典型如外設。Observers 可以對內存訪問進行觀察。內存屏障可以指定哪些 observers 可以在何時觀察到這些內存訪問。
譯者:這里值得再次重點注解一下,observe(觀察)一詞我個人覺得其實是比較蛋疼的,叫“perceive(感知)”可能會更容易讓人理解。所謂的“觀察到”一個內存被更新,指的就是對于這個 Observer 來說,其“感知到”該內存被更新了,也就是在該 Observer 對此內存發起 read 或 write 時,它已明確知曉此內存處最新的值。原文中還用了“visible(可見)”一詞,所謂“visible”,就是“可被觀察到的”。
一個內存 write 在到達內存系統中的某個點時,將變的“可見(visible)”。當 write 可見時,其對于內存屏障指令所指定 Shareability domain 上的所有 Observers 來說是一致的(譯者:就是大家看到的值一定是相同的)。假設一個 PE 對一個內存地址進行 write,如果其他 PE 在讀相同地址時可以觀察到更新后的值,則此 write 操作是“可被觀察到的”。舉例來說,如果內存是 Normal cacheable 的,則 write 操作會在到達 Shareability domain 的 coherent data caches(譯者:這么多定語,簡單理解為 cache 即可)時,成為“可被觀察到的”。
Armv8-A 內存模型被描述為 Other-multi-copy atomic。在一個 Other-multi-copy atomic 系統中,一個 Observer 對某地址的 write,如果可以被不同的 Observer 所觀察到,則對該地址進行訪問的所有其他 Observers,它們所觀察到的結果應該是一致的。但是,一個 Observer 在它的 writes 對系統中其他 Observers 可見之前,Observer 是可以觀察到其自己的 writes 的(譯者:在 store buffer 里面)。
工程實踐中,一個描述為 Other-multi-copy atomic 的內存模型,會允許 PEs 實現 local store buffers,這些 store buffers 并不會對系統中的其他 Observers 一致(譯者注:意思是 PE 自己能看到 store buffer 中的內容,但其他 PE 看不到),但會被用來做依賴關系的冒險檢查。Store Buffers(STBs) 微架構機制用來將一個 PE 的指令執行流水線與 Load/Store Unit(LSU) 解耦。
7. 數據內存屏障(data memory barriers)
Data Memory Barrier(DMB) 用于防止指定的 explicit 數據訪問會跨越屏障指令亂序。program order 上在 DMB 之前 的所有 explicit 數據 load 或 store 指令,會在 program order 上該 DMB 之后的數據訪問之前被指定 Shareability domain 中的所有 Observers 觀察到。
DMB 指令接受一個參數,該參數指明所需保序的 explicit 訪問所屬的 types,以及 Shareability domain 中保序所需面向的 Observers。相關討論在下文 "10. 內存屏障范圍的限制"。
以下代碼展示了在弱內存序模型下可能的亂序。X1 和 X3 地址處的內存初始為 0x0:
STR #1, [X1]
STR #1, [X3] ; Might be observed before the previous STR.
該例子中,X3 地址處內存的更新和被觀察到,可以發生在 X1 地址之前。現在假設另一個 Observer 以相同順序讀取兩個相同的內存地址,下表展示了內存系統可能會返回的觀察值組合:
下面的例子使用 DMB 指令來強制內存保序。X1 和 X3 地址處的內存初始為 0x0:
STR #1, [X1]
DMB
STR #1, [X3] ; Cannot observe this STR without first observing the previous STR.
該例子中,X3 地址處內存如果被觀察到已更新,則 X1 地址處內存必然也被觀察到已更新。現在假設另一個 Observer 以相同順序讀取兩個相同的內存地址,下表展示了內存系統可能會返回的觀察值組合:
下面是關于 DMB 的更多信息:
- 使用一個 DMB 可以在訪問之間創建一個順序。Armv8-A Architecture Reference Manual 將此順序稱為 Barrier-ordered-before。
- 對于 DMB 來說,data cache maintenance operations(譯者:就是 data invalidation 之類的那些指令)被視作 explicit 數據訪問指令,其遵循 DMB 的內存序限制。
注意:如果要用 DMB 指令來對 cache maintenance 指令進行保序,必須指定一個同時包含 loads 和 stores 的參數。
- DMB 無法保證訪問出現的時機。DMB 保證當訪問真正發生時,會采用屏障和其參數所定義的順序限制。DMB 允許 PE 在 explicit 數據等待完成期間繼續執行。
- DMB 不會阻止后續 explicit 數據 read 操作被投機執行。如果投機執行了一個 read,core 必須丟棄寄存器中的投機數據(譯者:這有點類似分支預測失敗了要 flush 流水線。這里表達的是,DMB 只會保證數據被“被觀察到”時的順序,而不保證微架構層面的執行順序,比如 DMB 后面的數據可能會被投機讀之類的)。在前面的所有 explicit 數據訪問被觀察到 之后 ,core 必須重新執行這個 load(譯者:因為投機錯了唄)。
8. 內存同步屏障(data synchronization barriers)
DSB 內存屏障用于確保在此 DSB 之前的內存訪問,必須在 DSB 指令執行完成之前完成。正因如此,其是一個比 DMB 約束更強的內存屏障。由指定參數的 DMB 所帶來的保序,相同參數的 DSB 也可以做到。
一個 PE 所執行的 DSB 會在如下情況執行完成:
- program order 上,在 DSB 之前的所有指定訪問類型的 explicit 內存訪問都已完成,指定 Shareability domain 中的其他 Observers 皆可觀察到。
- 如果 DSB 中所指定的參數是 reads 和 writes,則由 PE 在 DSB 之前發起的所有 cache maintenance 指令以及所有 TLB maintenance 指令都已完成(對于指定 Shareability domain 來說)。
同樣的,program order 上 DSB 指令之后的指令,都無法在 DSB 指令完成 之前 ,對系統狀態產生任何更改,或是發揮其任意部分功能(譯者:原文比較蛋疼,其實就是想表達“壓根沒有一丁點被執行到的可能”)。DSB 無法阻止對指令的預取和解碼。
下面的代碼展示 DSB 所帶來的保序效果:
STR X0, [X1] ; Must complete before the DSB can retire.
DSB
ADD X1, X2, X3 ; Must NOT be executed before the first STR completes.
STR X4, [X5] ; Must NOT be executed until the first STR completes.
上面代碼中的 DSB,可以確保第二條 STR 以及 ADD 指令不會在第一個 STR 以及 DSB 執行完成之前執行。
9. 一次訪問何時被認為是“已完成”?
上一節中提到,DSB 可以強制之前的由 DSB 參數所指定的內存訪問先完成(譯者:原文對 DSB 指令使用的描述動詞是 "retire")。那么一次內存訪問到底何時才被視為“已完成”?
read 的完成解釋起來要比 write 的完成要簡單一些。這是因為,一次 read 的完成點是所讀數據被返回到 PE 的 architectural 通用寄存器中。
一次 write 的完成要更復雜。對于一次對 Device 內存的 write 來說,write 的完成點取決于此 Device memory type 所指定的 Early-write acknowledgement 屬性。如果內存系統支持 Early-write acknowledgement,則 DSB 指令可以在 write 到達 end 外設之前完成(譯者:原文使用的動詞是 retire)。對于一個 Device-nGnRnE 的內存 write,只能在內存系統收到 end 外設的 write response 時才算完成。
下面的例子中,DSB 指令會一直阻塞執行,直到對 Device-nGnRnE 內存的 STR 操作從 end 外設收到對指定內存地址的 write response:
STR X0, [Device-nGnRnE] ; Must receive a write response from the end-peripheral
DSB SY
10. 內存屏障范圍的限制
DMB 和 DSB 內存屏障指令都需要一個參數,來指明內存屏障所要保序的內存訪問的 type 以及指令所作用的 Shareability domain。此參數所指定的范圍,決定了屏障指令的保序行為所影響的 Observers。
該對內存屏障影響范圍進行指定的能力,在內存屏障效果優化時會很有用。有些場景下,一個屏障的全量保序約束會太過嚴格(譯者:開銷會比較大)。如果限制一個屏障所能影響的內存訪問以及 Observers 范圍,會帶來微架構層面的優化,進而減少內存屏障對性能的影響。
注意:Armv8-A AArch64 體系結構要求在使用 DSB 或 DMB 時必須顯式定義參數。此約束與之前的版本不同,之前的版本在不指定明確參數的情況下會使用默認選項 SY。
下表是 DSB 和 DMB 的合法參數:
舉例來說,DMB ISHST 只會影響 explicit store 指令的順序,屏障兩側的 loads 順序不受影響。DMB 也只會在執行該指令的 PE 所在的 Inner Shareable domain 的 Observers 間進行保序。
考慮下面的例子:
PE0
LDR x0, [X4] ; Can be observed out-of-order
STR #1, [X1]
DMB ISHST
STR #1, [X3]
如果 PE0 和 PE1 不屬于同一 Shareable domain,那么架構上是允許 PE1 在觀察到 X1 地址處內存被更新之前先觀察到 X3 地址處內存被更新的。另外,所有 PEs(包括 PE0)會觀察到 X4 的 load 相對 X1 和 X3 的 write 是亂序的。
這種對保序范圍的縮小,會減少在 Observers 之間保序時的系統開銷。
11. 各種觀察者
以下在體系結構中被視為獨立的 Observers:
- core 的指令接口,通常稱為 Instruction Fetch Unit(IFU)。
- 數據接口,通常稱為 Load Store Unit(LSU)。
- MMU 頁表遍歷單元。
如“6. 啥是觀察者(Observer)”一節,一個 Observer 是可以發起內存訪問的部件,比如,MMU 會在遍歷頁表時發起 read。
AArch64 不會對不同 Observer 所發起的訪問進行保序,即使訪問間存在地址依賴。舉例來說,以下指令序列可能會亂序,即使它們之間存在依賴:
DC CVAU, X0 ; Operations are executed in any order
IC IVAU, X0 ; despite address dependency.
如果這些指令被亂序,指令 cache 可能會被填充進數據 cache 中的過期數據。為解決此問題,需要一個內存屏障。例子如下:
DC CVAU, X0 ; Operations are executed in any order
DSB ISH
IC IVAU, X0 ; despite address dependency.
該例子中,數據 cache clean(DC CVAU) 會在指令 cache invalidate(IC IVAU) 執行之前完成。DC CVAU 保證了在執行 invalidate 之前,新的數據總是對指令 cache 可見。
注意:這里需要 DSB 是因為 DMB 只會影響數據訪問,也就是只能影響到數據 cache maintenance 指令,而無法影響到 cache invalidate 指令。
12. load-acquire 與 store-release 指令
Armv8-A AArch64 提供了一組面向 loads 的帶有 Acquire 語義的指令,以及面向 stores 的帶有 Release 語義的指令。這些指令支持了 Release Consistency sequentially consistent(RCsc) 模型。
這些新的 load 和 store 指令包含了隱晦的屏障語義,有點類似單向屏障。這些指令相對 DMB 或 DSB 來說保序語義更弱,因為它們會影響內存屏障指令兩側的指定 explicit 內存訪問的順序。Load-Acquire 和 Store-Release 指令所引入的弱保序能力支持在微架構層面的優化,從而降低顯式內存屏障所帶來的性能影響。如果內存序可以通過 Load-Acquire 或 Store-Release 完成,則更推薦使用這些指令而不是 DMB。
Shareability domain 定義了這些指令保序所能影響的 Observers 范圍。Load-Acquire 和 Store-Release 所影響的 Shareability domain,就是該指令所訪問的地址的 Shareability domain(譯者:load-acquire 和 store-release 沒法像 DMB、DSB 那樣通過參數指定所要影響的 Shareability domain,只能是其訪問的地址屬于什么 Shareability domain 就是哪個 Shareability domain)。舉個例子,如果 PE0 和 PE1 不在同一個 Inner Shareable domain 中,那么下面的代碼中,如果 X3 是 Inner Shareable 的,則架構上會允許 PE1 在觀察到 X1 地址處內存被更新之前觀察到 X3 地址處內存被更新:
PE0
STR #1, [X1]
STLR #1, [X3]
下面是關于 Load-Acquire 和 Store-Release 指令的一些更多信息:
- 對于 Load-Acquire、Load-AcquirePC 以及 Store-Release 指令,所傳入的數據地址必須對齊到所要訪問的數據長度,否則訪問會觸發 Alignment fault。
- 對于 Load-Acquire Exclusive Pair 以及 Store-Release Exclusive Pair,所傳入的數據地址必須對齊到所要 load 的數據長度的兩倍。否則訪問會觸發 Alignment fault。如下面代碼所示:
LDAXP x0, x1, [0x08] ; Alignment fault
LDAXP x0, x1 [0x10]
- Load-Acquire 和 Store-Release 還有各自的獨家變體。
12.1 Load-Acquire
Load-Acquire LDAR 指令的保序規則如下:
- 所有 LDAR 之后的 explicit 內存訪問,會在 LDAR 之后被觀察到。
- 所有 LDAR 之前的 explicit 內存訪問不受影響,可以無視 LDAR 而亂序。
下圖展示了具體的保序規則:
12.2 Store-Release
Store-Release STLR 指令的保序規則如下:
- 所有 STLR 之前的 explicit 內存訪問,會在 STLR 之前被觀察到。
- 所有 STLR 之后的 explicit 內存訪問不受影響,可以無視 STLR 而亂序。
下圖展示了具體的保序規則:
下面的示例代碼,展示如何通過 STLR 來保序。X1 和 X3 地址處的內存初始為 0x0:
STR #1, [X1]
STLR #1, [X3] ; Cannot observe this STLR without observing the previous STR.
該示例中,如果 X3 地址處的內存被觀察到被更新了,則 X1 必然也被觀察到被更新了。現在假設另一個 Observer 以相同順序讀取兩個相同的內存地址,下表展示了內存系統可能會返回的觀察值組合:
12.3 Load-Acquire 和 Store-Release pairs
Load-Acquire 和 Store-Release 指令可以作為一個 pair 組合來對代碼臨界區進行保護。這些指令的組合使用,可以確保代碼臨界區內的訪問不會被亂序到臨界區之外。代碼臨界區之外的訪問(譯者:這里原文應該是筆誤了,原文是 accesses inside the critical code section)不受影響,可以被亂序,如下圖所示:
12.4 sequentially consistent
acquire/release 操作使用 sequentially consistent 模型。意思是,當一個 Load-Acquire 在 program order 上位于一個 Store-Release 之后時,則由 Store-Release 指令所發起的內存訪問,將先于 Load-Acquire 指令所發起的內存訪問被觀察到。下圖展示了這種保序約束:
12.5 Load-AcquirePC
Armv8.3-A 還提供了 Load-AcquirePC 指令。Load-AcquirePC 和 Store-Release 的組合使用,可以支持更弱的 Release Consistency processor consistent(RCpc) 模型,如下圖所示:
通過這些新的 Load-AcquirePC 指令,無需再遵守 Load-Acquires 必須在 Store-Release 之后被觀察到的約束(譯者:對比圖 9 看)。
12.6 Limited Ordering Regions
Armv8.1-A 添加了對 Limited Ordering Regions(LORegions)(譯者:受限的保序區域)的支持。LORegions 支持大型系統(譯者:此處所謂的“大型系統”,應該指的是內存規模比較大的系統,比如多 NUMA 系統)通過特殊的 Load-Acquire(LDLAR) 和 Store-Release(STLLR) 指令,來為“對指定物理地址(Physical Address,PA)映射的內存訪問”間進行保序。
LORegions 可以避免在等待一個內存訪問(針對內存映射中的任意地址,并對發起訪問的 PE 所屬的 Shareability domain 中的所有 Observers 可觀察)時的大量性能開銷。該場景(譯者:指的是引入性能開銷的場景)可能由現有的 Load-Acquire 和 Store-Release 指令引入(譯者:這里的意思是說,原先的 Load-Acquire、Store-Release 指令會導致有些訪問必須在屏障之前完成,導致 CPU 一直等待直至訪問完成而引入性能開銷)。此特性只在軟件明確知道哪些 Observers 希望共享一個內存地址時使用,此軟件通常可以知道系統的拓撲。舉個例子,下圖展示了一個多 socket 系統上,跨 socket 內存訪問會帶來極大的延遲:
通過合理的系統設計,對一個 socket 所使用物理內存的本地區域應用 limiting ordering(受限的保序),從而提升整體性能。
LORegions 只能被應用于 Non-secure 物理內存訪問。一個 LORegion 由一個 LORegion descriptor 描述。LORegion descriptors 的數量取決于具體的體系結構實現,可以通過讀取 LORID_EL1 寄存器獲取。
一個 LORegion descriptor 包含以下信息,通過系統寄存器來編程:
- 起始地址(LORSA_EL1)。
- 結束地址(LOREA_EL1)。
- LORegion 數量(LORN_EL1)。
- 表征 LORegion descriptor 是否合法的 valid bit(LORC_EL1)。
以下代碼展示對 2 個 LORegion 進行編程:
MOV x0, #0x2 // LORegion number
MOV x1, #0x80000000 // LORegion start address
MOV x2, #0xC0000000 // LORegion end address
MOV x3, #0x1 // LORegion enable (valid bit)
MSR LORN_EL1, x0 // Select the LORegion number descriptor
ISB
MSR LORSA_EL1, x1
MSR LOREA_EL1, x2
MSR LORC_EL1, x3
ISB
下圖中,只有對相同 LORegion 中地址(由 LDLAR 或 STLLR 指令指定)的訪問才會受影響,對 LORegion 之外的內存訪問不受影響。舉例來說,對 C 的 store 會先于 LDLAR 被觀察到。如果軟件使用了一個 LDAR,則對 C 的 store 會在 LDAR 之后被觀察到,如圖中所示:
13. 指令屏障(instruction barriers)
Arm 體系結構下 PE 的上下文包括 caches、TLBs 以及系統寄存器的狀態。對 cache 或 TLB 的 maintenance 操作或對系統寄存器的更新屬于一種上下文變更(context-changing)操作。
體系結構只保證一個上下文變更操作在一個上下文同步(context synchronization)事件之后被觀察到(譯者:原文這里用的動詞是 seen,不是 observed,我這里翻譯成“觀察”不知是否恰當,也許翻譯為“生效”更佳。這里想表達的意思應該是,在做完上下文變更操作之后,必須再觸發一個上下文同步事件,此變更方能生效)。
對 explicit 上下文同步的約束,讓處理器設計者無需在每個 cycle 上傳播所有上下文變更(譯者注:意思就是把問題拋給程序員了從而解放了處理器的設計者,必須由程序進行 explicit 的上下文同步事件之后,上下文變更才生效,否則就必須要 CPU 在每個 cycle 上主動做一次上下文變更同步,這個就 heavy 多了),implicit 上下文同步是非必要的開銷(譯者:意思就是讓 CPU 在每個 cycle 上做上下文同步 —— 這就是 implicit 上下文同步 —— 會引入無謂的開銷)。故軟件在希望應用一個新的上下文時,需要顯式發起一個上下文同步事件。
以下事情中任一為一個上下文同步事件:
- 執行一個 ISB 操作。
- 觸發異常(譯者:takes an exception,這里的 take 應該翻譯為“觸發”還是“處理”,筆者拿不準)。
- 從一個異常中返回。
- 從 Debug 狀態中退出。
注意:Arm 處理器實現,允許只要 PE 不發出上下文同步事件,就始終不為后續的執行更新它們的上下文。
執行一個上下文同步事件可以確保:
- 所有在上下文同步事件執行時間點上 pending 的 umasked 中斷,都可以在上下文同步事件后的第一條指令之前被處理。
- program order 上,任意位于一個可以觸發上下文同步事件指令之后的指令,在上下文同步事件發生 之前 ,皆不能發揮其任意部分功能((譯者:其實就是想表達“壓根沒有一丁點被執行到的可能”))。
- 在上下文同步事件之前的所有系統寄存器 writes,都會影響 program order 上位于觸發上下文同步事件的指令之后的所有指令(譯者:因為根據定義,對系統寄存器的 write 就是一種上下文變更,而上下文變更在上下文同步事件之后就會生效,所以會影響到后續指令。下面兩條同理)。
- 所有對頁表的已完成變更,如果其 entries 在變更 之前 ,不允許緩存在一個 TLB 中,會影響所有在 program order 上位于觸發上下文同步事件指令之后的指令預取。
- 所有在上下文同步事件之前完成的 TLBs、指令 cache 以及 AArch32 狀態下的分支預測 invalidation,會影響所有在 program order 上位于觸發上下文同步事件指令之后的指令。
13.1 使用示例
假設軟件必須先確保對 SVE、Advanced SIMD 以及浮點寄存器的訪問不會 trapped(譯者:這里 trapped 我不明白是指觸發異常還是會導致 VMExit)。舉個例子,EL1 下運行時,對 SVE、Advanced SIMD 以及浮點寄存器訪問的 trapping,可以通過將 CPACR_EL1.FPEN 編程為 0x3 來禁能,如下面的代碼所示:
MRS X1, CPACR_EL1
ORR X1, X1, #(0x3 < < 20) ; Write CPACR_EL1.FPEN bits
MSR CPACR_EL1, X1
ISB
FADD S0, S1, S2
如果沒有 ISB 指令,則對 trapping 的禁能(是一個上下文變更操作)并不保證能被 FADD 指令觀察到(譯者:would not be guaranteed to be seen by the FADD instruction,這里原文用的又是 seen,理解為“在 FADDR 指令執行時此上下文變更已生效”)。不加 ISB 的話會導致 FADD 指令觸發一個 Synchronous 異常。本場景下,ISB 作為一個上下文同步事件,其用來確保新的上下文(此“新的上下文”也就是對 SVE、Advanced SIMD 以及浮點寄存器 trap 的禁能)可以被 FADD 指令觀察到。
本例子中,如果在對 SVE、Advanced SIMD 以及浮點寄存器進行訪問之前,從 EL1 返回到了 EL0,則無需 ISB。這是因為從 EL1 到 EL0 的異常返回也是一個上下文同步事件。
14. 知識檢驗
Q1:啥是 Observer?
A1:Observer 是一個處理單元或系統部件,比如外設,可以向內存發起讀寫。
Q2:如果我要確保由一個 DMB 分開的兩個 stores,被同 Inner Shareable domain 中的其他 Observers 按序觀察到,應該使用啥參數?
A2:DMB ISHST。
Q3:如果要確保此前的內存訪問都已完成后再繼續執行,應該使用什么內存屏障?
A3:DSB。
Q4:啥是 architecturally 定義的上下文同步事件?
A4:architecturally 定義的上下文同步事件包括以下:
- 執行一個 ISB 操作。
- 觸發一個異常。
- 從一個異常中返回。
- 從 Debug 狀態返回。
評論
查看更多