Cache coherency
Cacheability
Normal memory 可以設(shè)置為 cacheable 或 non-cacheable,可以按 inner 和 outer 分別設(shè)置。
Shareability
設(shè)置為 non-shareable 則該段內(nèi)存只給一個(gè)特定的核使用,設(shè)置為 inner shareable 或 outer shareable 則可以被其它觀測(cè)者訪問(wèn)(其它核、GPU、DMA 設(shè)備),inner 和 outer 的區(qū)別是要求 cache coherence 的范圍,inner 觀測(cè)者和 outer 觀測(cè)者的劃分是 implementation defined。
PoC & PoU
當(dāng) clean 或 invalidate cache 的時(shí)候,可以操作到特定的 cache 級(jí)別,具體地,可以到下面兩個(gè)“點(diǎn)”:
Point of coherency(PoC):保證所有能訪問(wèn)內(nèi)存的觀測(cè)者(CPU 核、DSP、DMA 設(shè)備)能看到一個(gè)內(nèi)存地址的同一份拷貝的“點(diǎn)”,一般是主存。
Point of unification(PoU):保證一個(gè)核的 icache、dcache、MMU(TLB)看到一個(gè)內(nèi)存地址的同一份拷貝的“點(diǎn)”,例如 unified L2 cache 是下圖中的核的 PoU,如果沒(méi)有 L2 cache,則是主存。
當(dāng)說(shuō)“invalidate icache to PoU”的時(shí)候,是指 invalidate icache,使下次訪問(wèn)時(shí)從 L2 cache(PoU)讀取。
PoU 的一個(gè)應(yīng)用場(chǎng)景是:運(yùn)行的時(shí)候修改自身代碼之后,使用兩步來(lái)刷新 cache,首先,clean dcache 到 PoU,然后 invalidate icache 到 PoU。
Memory consistency
ARMv8-A 采用弱內(nèi)存模型,對(duì) normal memory 的讀寫(xiě)可能亂序執(zhí)行,頁(yè)表里可以配置為 non-reordering(可用于 device memory)。
Normal memory:RAM、Flash、ROM in physical memory,這些內(nèi)存允許以弱內(nèi)存序的方式訪問(wèn),以提高性能。
單核單線程上連續(xù)的有依賴的 str 和 ldr 不會(huì)受到弱內(nèi)存序的影響,比如:
str x0, [x2]
ldr x1, [x2]
Barriers
ISB
刷新當(dāng)前 PE 的 pipeline,使該指令之后的指令需要重新從 cache 或內(nèi)存讀取,并且該指令之后的指令保證可以看到該指令之前的 context changing operation,具體地,包括修改 ASID、TLB 維護(hù)指令、修改任何系統(tǒng)寄存器。
DMB
保證所指定的 shareability domain 內(nèi)的其它觀測(cè)者在觀測(cè)到 dmb 之后的數(shù)據(jù)訪問(wèn)之前觀測(cè)到 dmb 之前的數(shù)據(jù)訪問(wèn):
str x0, [x1]
dmb
str x2, [x3] // 如果觀測(cè)者看到了這行 str,則一定也可以看到第 1 行 str
同時(shí),dmb 還保證其后的所有數(shù)據(jù)訪問(wèn)指令能看到它之前的 dcache 或 unified cache 維護(hù)操作:
dc csw, x5
ldr x0, [x1] // 可能看不到 dcache clean
dmb ish
ldr x2, [x3] // 一定能看到 dcache clean
DSB
保證和 dmb 一樣的內(nèi)存序,但除了訪存操作,還保證其它任何后續(xù)指令都能看到前面的數(shù)據(jù)訪問(wèn)的結(jié)果。
等待當(dāng)前 PE 發(fā)起的所有 cache、TLB、分支預(yù)測(cè)維護(hù)操作對(duì)指定的 shareability domain 可見(jiàn)。
可用于在 sev 指令之前保證數(shù)據(jù)同步。
一個(gè)例子:
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 可以通過(guò) 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 原子操作只保證原子性,不保證同步語(yǔ)義。
// 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 指令,可能被亂序執(zhí)行,有可能最終產(chǎn)生 r1 == r2 == 42 的結(jié)果,即 A 看到了 D,C 看到了 B。
典型的 relaxed ordering 的使用場(chǎng)景是簡(jiǎn)單地增加一個(gè)計(jì)數(shù)器,例如 std::shared_ptr 中的引用計(jì)數(shù),只需要保證原子性,沒(méi)有 memory order 的要求。
Release-acquire
Rel-acq 原子操作除了保證原子性,還保證使用 release 的 store 和使用 acquire 的 load 之間的同步,acquire 時(shí)必可以看到 release 之前的指令,release 時(shí)必看不到 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 這個(gè)寫(xiě)操作。
這段代碼在 ARM 上會(huì)編譯成使用 stlr 和 ldar ,但其實(shí) C++ 所定義的語(yǔ)義比 stlr 和 ldar 實(shí)際提供的要弱,C++ 只保證使用了 release 和 acquire 的兩個(gè)線程間的同步。
典型的 rel-acq ordering 的使用場(chǎng)景是 mutex 或 spinlock,當(dāng)釋放鎖的時(shí)候,釋放之前的臨界區(qū)的內(nèi)存訪問(wèn)必須都保證對(duì)同時(shí)獲取鎖的觀測(cè)者可見(jiàn)。
Release-consume
和 rel-acq 相似,但不保證 consume 之后的訪存不會(huì)在 release 之前完成,只保證 consume 之后對(duì) consume load 操作有依賴的指令不會(huì)被提前,也就是說(shuō) consume 之后不是臨界區(qū),而只是使用 release 之前訪存的結(jié)果。
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 到非空指針之前執(zhí)行,這時(shí)候不保證能看到 release store,也就不保證能看到 data = 42 。
Sequentially-consistent
Seq-cst ordering 和 rel-acq 保證相似的內(nèi)存序,一個(gè)線程的 seq-cst load 如果看到了另一個(gè)線程的 seq-cst store,則必可以看到 store 之前的指令,并且 load 之后的指令不會(huì)被 store 之前的指令看到,同時(shí),seq-cst 還保證每個(gè)線程看到的所有 seq-cst 指令有一個(gè)一致的 total order。
典型的使用場(chǎng)景是多個(gè) producer 多個(gè) consumer 的情況,保證多個(gè) 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 的賦值順序,所以必至少有一個(gè)執(zhí)行到 ++z 。
Seq-cst 和其它 ordering 混用時(shí)可能出現(xiàn)不符合預(yù)期的結(jié)果,如下面例子中,對(duì) thread 1 來(lái)說(shuō),A sequenced before B,但對(duì)別的線程來(lái)說(shuō),它們可能先看到 B,很遲才看到 A,于是 C 可能看到 B,得到 r1 = 1 ,D 看到 E,得到 r2 = 3 ,F(xiàn) 看不到 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
評(píng)論