Cache coherency
Cacheability
Normal memory 可以設置為 cacheable 或 non-cacheable,可以按 inner 和 outer 分別設置。
Shareability
設置為 non-shareable 則該段內存只給一個特定的核使用,設置為 inner shareable 或 outer shareable 則可以被其它觀測者訪問(其它核、GPU、DMA 設備),inner 和 outer 的區別是要求 cache coherence 的范圍,inner 觀測者和 outer 觀測者的劃分是 implementation defined。
PoC & PoU
當 clean 或 invalidate cache 的時候,可以操作到特定的 cache 級別,具體地,可以到下面兩個“點”:
Point of coherency(PoC):保證所有能訪問內存的觀測者(CPU 核、DSP、DMA 設備)能看到一個內存地址的同一份拷貝的“點”,一般是主存。
Point of unification(PoU):保證一個核的 icache、dcache、MMU(TLB)看到一個內存地址的同一份拷貝的“點”,例如 unified L2 cache 是下圖中的核的 PoU,如果沒有 L2 cache,則是主存。
當說“invalidate icache to PoU”的時候,是指 invalidate icache,使下次訪問時從 L2 cache(PoU)讀取。
PoU 的一個應用場景是:運行的時候修改自身代碼之后,使用兩步來刷新 cache,首先,clean dcache 到 PoU,然后 invalidate icache 到 PoU。
Memory consistency
ARMv8-A 采用弱內存模型,對 normal memory 的讀寫可能亂序執行,頁表里可以配置為 non-reordering(可用于 device memory)。
Normal memory:RAM、Flash、ROM in physical memory,這些內存允許以弱內存序的方式訪問,以提高性能。
單核單線程上連續的有依賴的 str 和 ldr 不會受到弱內存序的影響,比如:
str x0, [x2]
ldr x1, [x2]
Barriers
ISB
刷新當前 PE 的 pipeline,使該指令之后的指令需要重新從 cache 或內存讀取,并且該指令之后的指令保證可以看到該指令之前的 context changing operation,具體地,包括修改 ASID、TLB 維護指令、修改任何系統寄存器。
DMB
保證所指定的 shareability domain 內的其它觀測者在觀測到 dmb 之后的數據訪問之前觀測到 dmb 之前的數據訪問:
str x0, [x1]
dmb
str x2, [x3] // 如果觀測者看到了這行 str,則一定也可以看到第 1 行 str
同時,dmb 還保證其后的所有數據訪問指令能看到它之前的 dcache 或 unified cache 維護操作:
dc csw, x5
ldr x0, [x1] // 可能看不到 dcache clean
dmb ish
ldr x2, [x3] // 一定能看到 dcache clean
DSB
保證和 dmb 一樣的內存序,但除了訪存操作,還保證其它任何后續指令都能看到前面的數據訪問的結果。
等待當前 PE 發起的所有 cache、TLB、分支預測維護操作對指定的 shareability domain 可見。
可用于在 sev 指令之前保證數據同步。
一個例子:
str x0, [x1] // update a translation table entry
dsb ishst // ensure write has completed
tlbi vae1is, x2 // invalidate the TLB entry for the entry that changes
dsb ish // ensure that TLB invalidation is complete
isb // synchronize context on this processor
DMB & DSB options
dmb 和 dsb 可以通過 option 指定 barrier 約束的訪存操作類型和 shareability domain:
One-way barriers
- Load-Acquire (LDAR): All loads and stores that are after an LDAR in program order, and that match the shareability domain of the target address, must be observed after the LDAR.
- Store-Release (STLR): All loads and stores preceding an STLR that match the shareability domain of the target address must be observed before the STLR.
- LDAXR
- STLXR
Unlike the data barrier instructions, which take a qualifier to control which shareability domains see the effect of the barrier, the LDAR and STLR instructions use the attribute of the address accessed.
C++ & Rust memory order
Relaxed
Relaxed 原子操作只保證原子性,不保證同步語義。
// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D
上面代碼在 ARM 上編譯后使用 str 和 ldr 指令,可能被亂序執行,有可能最終產生 r1 == r2 == 42 的結果,即 A 看到了 D,C 看到了 B。
典型的 relaxed ordering 的使用場景是簡單地增加一個計數器,例如 std::shared_ptr 中的引用計數,只需要保證原子性,沒有 memory order 的要求。
Release-acquire
Rel-acq 原子操作除了保證原子性,還保證使用 release 的 store 和使用 acquire 的 load 之間的同步,acquire 時必可以看到 release 之前的指令,release 時必看不到 acquire 之后的指令。
#include < thread >
#include < atomic >
#include < cassert >
#include < string >
std::atomic< std::string * > ptr;
int data;
void producer() {
std::string *p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer() {
std::string *p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
上面代碼中,一旦 consumer 成功 load 到了 ptr 中的非空 string 指針,則它必可以看到 data = 42 這個寫操作。
這段代碼在 ARM 上會編譯成使用 stlr 和 ldar ,但其實 C++ 所定義的語義比 stlr 和 ldar 實際提供的要弱,C++ 只保證使用了 release 和 acquire 的兩個線程間的同步。
典型的 rel-acq ordering 的使用場景是 mutex 或 spinlock,當釋放鎖的時候,釋放之前的臨界區的內存訪問必須都保證對同時獲取鎖的觀測者可見。
Release-consume
和 rel-acq 相似,但不保證 consume 之后的訪存不會在 release 之前完成,只保證 consume 之后對 consume load 操作有依賴的指令不會被提前,也就是說 consume 之后不是臨界區,而只是使用 release 之前訪存的結果。
Note that currently (2/2015) no known production compilers track dependency chains: consume operations are lifted to acquire operations.
#include < thread >
#include < atomic >
#include < cassert >
#include < string >
std::atomic< std::string * > ptr;
int data;
void producer() {
std::string *p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer() {
std::string *p2;
while (!(p2 = ptr.load(std::memory_order_consume)))
;
assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr
assert(data == 42); // may or may not fire: data does not carry dependency from ptr
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
上面代碼中,由于 assert(data == 42) 不依賴 consume load 指令,因此有可能在 load 到非空指針之前執行,這時候不保證能看到 release store,也就不保證能看到 data = 42 。
Sequentially-consistent
Seq-cst ordering 和 rel-acq 保證相似的內存序,一個線程的 seq-cst load 如果看到了另一個線程的 seq-cst store,則必可以看到 store 之前的指令,并且 load 之后的指令不會被 store 之前的指令看到,同時,seq-cst 還保證每個線程看到的所有 seq-cst 指令有一個一致的 total order。
典型的使用場景是多個 producer 多個 consumer 的情況,保證多個 consumer 能看到 producer 操作的一致 total order。
#include < thread >
#include < atomic >
#include < cassert >
std::atomic< bool > x = {false};
std::atomic< bool > y = {false};
std::atomic< int > z = {0};
void write_x() {
x.store(true, std::memory_order_seq_cst);
}
void write_y() {
y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y() {
while (!x.load(std::memory_order_seq_cst))
;
if (y.load(std::memory_order_seq_cst)) {
++z;
}
}
void read_y_then_x() {
while (!y.load(std::memory_order_seq_cst))
;
if (x.load(std::memory_order_seq_cst)) {
++z;
}
}
int main() {
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join(); b.join(); c.join(); d.join();
assert(z.load() != 0); // will never happen
}
上面的代碼中,read_x_then_y 和 read_y_then_x 不可能看到相反的 x 和 y 的賦值順序,所以必至少有一個執行到 ++z 。
Seq-cst 和其它 ordering 混用時可能出現不符合預期的結果,如下面例子中,對 thread 1 來說,A sequenced before B,但對別的線程來說,它們可能先看到 B,很遲才看到 A,于是 C 可能看到 B,得到 r1 = 1 ,D 看到 E,得到 r2 = 3 ,F 看不到 A,得到 r3 = 0 。
// Thread 1:
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// Thread 2:
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// Thread 3:
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F
評論
查看更多