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