---
# System prepended metadata

title: Arm Programmer's Guide X - Memory Ordering 學習筆記
tags: [Arm]

---

# Arm Programmer's Guide X - Memory Ordering 學習筆記

<h1>1. 前言 </h1>

此筆記為學習 [ARM® Cortex™-A Series Programmer's Guide
Version: 4.0 中第十章 Memory Ordering ](https://developer.arm.com/documentation/den0013/d/Memory-Ordering?lang=en) 的心得筆記。主旨在解釋 Armv7 對於記憶體存取 (Memory access) 順序的行為。
會需要特別一章節來解釋是因為一般來說 (在 Normal 類型的記憶體區段中) 實際的記憶體存取順序其實和程式的呼叫順序是不一致的。會有這現象又是因為 CPU 對效能在硬體上的優化技術。由於 CPU 的執行速度遠高於對記憶體的存取速度，所以 CPU 為了能將延遲降到最低、將效能拉到最高，會盡量的使用快取 (Cache) 或是寫緩衝 (Write Buffer)，這樣的結果就是執行順序並不一定會照著程式的執行順序。舉一個簡單的例子就是，在做寫入時，資料被寫入 Write Buffer 後 CPU 就會接著執行下一個指令，導致資料實際上從 Write Buffer 被寫入記憶體的時間會晚於他的下一個指令。
再舉一個跟讀有關的例子之前，需要先提到另一個技術較做 Speculative Access，這技術會讓 CPU 試著去預判接下來要執行的指令，然後提前先去執行，如果預判對了，就可以直接使用提前執行的結果；如果錯了，則丟棄該結果，重新執行正確的指令。這技術常被應用在分支之上，CPU 會試著先去預判要走到哪一個分支，如果預判正確了便能提升效率。回到原本的 Memory Access 順序的範例，當 CPU 也可能因為預判到後方的讀指令而預先執行，導致其實際執行順序其實早於在程式上比他還早的指令。
上述的兩個例子並沒有包含所有指令順序被調換的原因，事實上原因複雜到對軟體設計師來說，只需要把他們當成隨機調換的就好。
另外值得一提的是，上面提到的順序調換機制是在 CPU 硬體層面上實現的，也就是 CPU 拿到指定順序的指令，他並不一定會照著順序走。這和編譯器層面的順序調換是不一樣的，編譯器在將人寫的高階程式語言 (C 或者組合語言) 編譯成機器語言時，也會因為類似的效能優化原因，將執行順序做調換。而本文討論的順序問題都是針對前者，和編譯器的無關。

<h1>2. Memory Type</h1>

Memory Type 有分以下三種：
1. Normal
2. Strong-ordered
3. Device

|Memory Type      | Shareable| Cacheable | Description |
|-----------------|----------|-----------|-------------|
| Normal          | Yes      | Yes       | 能在不同核心之間共享的記憶體區段，其執行順序是不被保證的。 |
| Normal          | No       | Yes       | 只給特定單一核心使用的記憶體區段，其執行順序是不被保證的。 |
| Device          | -        | No        | 針對 Memory-mapping 周邊 (Peripheral) 設計的記憶體類型，其實際執行順序是保證和程式呼叫順序一致 |
| Strongly-ordered | -        | No        | 實際執行順序是保證和程式呼叫順序一致。所有的 Strongly-ordered 記憶體區段在核間都是共享的 |

(所謂的 memory-mapping peripheral 是指可以被映射到記憶體空間的設備，CPU 可以透過一般的記憶存取指令來操作該設備。相對於 memory-mapping peripheral 的是 private port peripheral，也就是需要透過 IN / OUT 指令來操作的周邊設備。)

Normal Memory 沒有辦法保證實際的存取順序，但是 Device 和 Strong-ordered 可以。也就是說，如果有兩個區段的存取，只要他們至少其中之一是 Noraml memory type，他們的存取順序就是無法保證的；相反的，只要他們都不是 Normal memory type，那他們就會依照執行順序去存取記憶體。

<h2>2.1 Strongly-Ordered 和 Device Memory</h2>
    
這兩個類型的記憶體空間除了執行順序保證和程式呼叫順序一樣外，還有以下幾個共同點：
1. CPU 會忠實地執行程式呼叫的記憶體存取，不會去做任何優化。也就是說，就算程式試著對某個記憶體位置連續做兩次讀取或寫入，CPU 就會真的做兩次，不會把它簡化成一次。如果程式想要讀 8 bit 的空間，CPU 就只會對這 8 bit 做讀取，不會一次讀 32 bit，然後再返回其中 8 bit 的值。
2. 所有的存取都是原子性的 (Atomic)，無法途中被中斷。
3. 不論讀寫都有可能對系統產生副作用 (Side-effect)。這邊的副作用應該是指說因為 Strongly-Ordered 和 Device 記憶體空間常常是外部設備的記憶體映射位置，對該空間做讀寫，有可能會對系統產生影響。舉例來說，對 DMA 的某個 Bit 做寫的動作，可能會觸發 DMA 去改寫記憶體空間。
4. 指令無法被 Cache，且也不支援 Speculative Access。所以這兩個類型才很適合用來作為外設映射記憶體空間類型，因為這樣的記憶體空間不是單純的儲存空間，就算針對某個一位置重複讀寫都有可能會有不同的意義，如果將結果都 Cache 起來，就會得到錯誤的結果。
5. 存取的位置一定要對齊 (Aligned)。假設系統是 32 bit (4 byte) 對齊的，那就沒辦法對 0x00000001 讀取，因為 0x00000001 沒有對齊 4 byte。
6. Device memory 的順序維持僅限於同一個外部設備的記憶體空間中，其大小至少為 1 KB。
7. Normal 記憶體空間的存取還是可以和 Stongly-ordered 或 Memory 記憶體空間存取調換順序。這也是前面 2. Memory Type 提到的 - 如果有兩個區段的存取，只要他們至少其中之一是 Noraml memory type，他們的存取順序就是無法保證的。

而他們的相異點只有一個：
Device memory 是可以寫緩衝的 (Bufferable)。也就是說，Device Memory 允許其 Memory Entry 的 B Bit (BIT[2]) 為 1。在這樣的情況下，寫指令只要寫到緩衝區就算完成，CPU 可以繼續執行下一個記憶體存取動作。這會導至執行順序可能不再是依照程式順序的 (下一個讀指令有可能比前一個寫指令真的寫進記憶體中還要早，但僅限於此，相對於 Normal Memory 的重新排序程度會小很多)，但這樣能提供更高的效率。而 Strongly-Ordered Memory 是完全不允許 Buffer 或是 Cache 存在的，任何讀寫都是直接對記憶體生效，且順序必定是照著程式的順序走。相關的表格可以參考 [Arm Programmer's Guide IX - The Memory Management Unit 學習筆記](https://hackmd.io/2nnVV8vISrO6ywClfJFHjQ?view) 4.3.2 章節中的第二個表格。
如果 Memory Device 的 Buffer 功能是被關閉的，他基本上行為和 Strongly-Ordered 是一樣的。

<h2>2.2 Normal Memory</h2>

Normal Memory 有以下幾個特性：
1. 讀寫指令可以重複執行也不用擔心對系統產生副作用。
2. 能夠執行 Speculative Access 和合併多個指令來提升效率。
3. 存取的位置可以不需要對齊 (Align)。
4. 能夠使用 Cache。Cache 有分 Inner 和 Outer Cache，Inner Cache 為 L1 或 L2 的 Cache，會較先被查詢；Outer Cache 則為 L2 或 L3，較晚被查詢。
5. 能夠設定是否和其他核共享 (Shareable)，這邊指的共享是指硬體能夠自動地去處理不同核間的一致性問題(例如他們的 Cache 會同步)。就算沒有設定共享，其他核其實也可以摸到該記憶體空間，只是開發者會需要自行維護不同核操作間的一致性。
6. 共享又分內部共享 (Inner Shareable) 和外部共享 (Outer Shareable)：內部共享區域 (Inner Shareable Domain) 是指多個處理器之間共享的區域，其他的核心無法接觸該區域。舉例來說，一個 Cortex-A15 處理器可以和另一個 Cortex-A7 處理器組成內部共享區域；外部共享區域 (Outer Shareable Domain) 則是處理器與外部設備的共享區域。舉例來說，一個 Cortex-A7 處理器可以和 GPU 組成外部共享區域。

<h1>3. 記憶體屏障 (Memory Barriers)</h1>

大部分時候我們都可以信任硬體去重排執行順序來達到效能的提升，但有的時候我們會想在關鍵的地方能夠精準地控制執行順序，這時候記憶體屏障就能派上用場。我們先看以下的例子：
Core A:
```c=
    STR R0, [Msg]    @ write some new data into postbox
    STR R1, [Flag]   @ new data is ready to read
```
Core B:
```c=
Poll_loop:
    LDR R1, [Flag]
    CMP R1, #0       @ is the flag set yet?
    BEQ Poll_loop
    LDR R0, [Msg]    @ read new data
```
由於執行順序的不可保證性，上方的程式極有可能不會照個預期的執行。Core A 有可能在把資料 (R0) 填到 Msg 前就先把 Flag 改舉起來；Core B 也有可能在確認到 Flag 被舉起來之前，就先把 Msg 中的值給讀了出來。
在這個例子中，我們會希望 Core A 執行第二行前能先執行第一行、Core B 在執行第三行前能夠先執行第二行，我們可以透過加上記憶體屏障指令 DMB 和 DSB 來達到此目的：
Core A:
```c=
    STR R0, [Msg]    @ write some new data into postbox
    DMB
    STR R1, [Flag]   @ new data is ready to read
```
Core B:
```c=
Poll_loop:
    LDR R1, [Flag]
    DSB
    CMP R1, #0       @ is the flag set yet?
    BEQ Poll_loop
    LDR R0, [Msg]    @ read new data
```
DMB 能夠確保他之前的指令在他之後的指令前完成、而 DSB 能確保他之前的指令都完成後才繼續往下走。
以下為更詳細的記憶體屏障指令說明：

<h2>3.1 指令說明</h2>

1. DSB (Data Synchronization Barrier)
顧名思義，在 DSB 之前的所有資料存取 (Explicit Data Access) 都需要先全部完成 (讀取完成、寫入進「記憶體」生效)，才可以繼續往下走。但指令的 Pre-fetch 不在此限制。
2. DMB (Data Memory Barrier)
和 DSB 有點像但是較為寬鬆。他將指令分成兩個區域：「 DMB 之前」和「 DMB 之後」，他會確保 「 DMB 之前的指令」一定會在「 DMB 之後的指令」前完成。但他並不像 DSB 一樣會阻擋指令執行下去，當執行到 DMB 時，就算 「DMB 之前的指令」還沒完成，還是會繼續往下執行，只要確保之後的流程中 DMB 前後指令的順序就好。
同樣地，這只限於資料的存取 (Explicit Data Access)、不限於指令的 Pre-fetch。
3. ISB (Instruction Synchronization Barrier)
前兩者都是針對資料的存取， ISB 則是針對指令的 Pre-fetch。在 ISB 會強制清空( Flush ) Pipeline 和 Prefetch Buffer，使其之後的指令都一定是從 Cache 或記憶體中取得。對於完整的資料 和指令屏障，可以用 ISB 搭配 DMB 。

DMB 和 DSB 還可以和以下的幾個指令做搭配：
1. SY：預設值。表示這個屏障會套用給整個系統，包含所有核心還有周邊設備。
3. ST：只針對寫指令( Store )做屏障。
4. ISH：只套用給內部共享區域 (Inner Shareable Domain)。
5. ISHST：ISH + ST。
6. NSH：A Barrier to the Point of Unification (PoU)。(這邊還看不懂，需要先看完原文第八章 Cache。)
7. NSHST：NSH + ST。
8. OSH：只套用給外部共享區域 (Outer Shareable Domain)。
9. OSHST：OSH + ST。

若想要在編譯器層級指定執行順序，Linux 可以呼叫 barrier() 來達到：
```c=
    write_to_hw_reg();
    barrier();
    read_from_hw_reg();
```
上方的代碼防止編譯器將讀取動作重新排列至寫指令之前，但執行時硬體仍然可以因為 speculative access 等原因提早執行讀取指令。

<h2>3.2 使用場景</h2>

<h3>3.2.1 避免 Deadlock </h3>

```=
    STR R0, [Addr]    @ write a command to a peripheral register
Poll_loop:
    LDR R1, [Flag]
    CMP R1, #0        @ wait for an acknowledge/state flag to be set
    BEQ Poll_loop
```

上述例子在沒有 Multiprocessing extensions 的情況下是很有可能造成 Deadlock 的。原因為第一行的寫指令有可能停留在 Write Buffer，但 CPU 一直忙於 Polling 迴圈中。
解法可以是在第一二行間插入一個 DSB。
(如果有啟用 Multiprocessing extensions，那 Write buffer 會需要在指定的時間內去真的寫進記憶體中，那麼沒有 DSB 也不會造成 deadlock。)

<h3>3.2.2 SEV 之前</h3>

WFE (Wait For Event) 和 WFI (Wait For Interrupt) 能夠使核心進入一種低功號的狀態。這在等待其他核心資源的時候很常使用。當持有該資源的核心要釋放該資源時，可以使用 SEV (Send Event) 指令去喚醒其他在 WFE 等待狀態中的核心。這時候如果 SEV 前沒有加上記憶體屏障，就有可能造成資源實際上還沒被釋放就觸發 SEV 指令，進而造成問題。
另外由於 SEV 不是資料存取指令，所以其之前的記憶體屏障一定要用 DSB 而不能用 DMB，因為 DMB 只在乎其前後資料存取間的順序，不在乎和其他指令間的順序關係，只有 DSB 能夠強迫其之前的所以資料存取都完成，才能接續執行接下來的任何指令。

<h2>3.3 Linux 的記憶體屏障 API</h2>

1. rmb(): rmb 為 Read Memory Barrier 的縮寫。此 API 能強迫他之前的所有讀指令比他之後的讀指令還早完成。
2. wmb(): wmb 為 Write Memory Barrier 的縮寫。此 API 能強迫他之前的所有寫指令比他之後的寫指令還早完成。
3. mb(): mb 為 Memory Barrier 的縮寫。此 API 能強迫他之前的所有讀寫指令比他之後的讀寫指令還早完成。

以上三個 API 都有其各自的 SMP 版本 (smp_rmb()、smp_wmb() 和 smp_mb())，他們用來在相同叢 (cluster) 內的核心之間設定記憶體屏障。

---

上一篇: [Arm Programmer's Guide IX - The Memory Management Unit 學習筆記](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Memory_Management_Unit_Studying_Note)
下一篇: [Arm Programmer's Guide XI - Exception Handling 學習筆記](https://hackmd.io/@uMqav-XESBCsrtZEx_Jpug/Arm_Programmer_Guide_Exception_Handling_Study_Note)