Cache coherency
Cacheability
Normal memory可以設(shè)置為cacheable或non-cacheable,可以按inner和outer分別設(shè)置。
Shareability
設(shè)置為non-shareable則該段內(nèi)存只給一個特定的核使用,設(shè)置為inner shareable或outer shareable則可以被其它觀測者訪問(其它核、GPU、DMA 設(shè)備),inner和outer的區(qū)別是要求cache coherence的范圍,inner觀測者和outer觀測者的劃分是implementation defined。
圖1
PoC&PoU
當(dāng)clean或invalidate cache的時候,可以操作到特定的cache級別,具體地,可以到下面兩個“點”:
Point of coherency(PoC):保證所有能訪問內(nèi)存的觀測者(CPU 核、DSP、DMA 設(shè)備)能看到一個內(nèi)存地址的同一份拷貝的“點”,一般是主存。
圖2
Point of unification(PoU):保證一個核的icache、dcache、MMU(TLB)看到一個內(nèi)存地址的同一份拷貝的“點”,例如unified L2 cache是下圖中的核的PoU,如果沒有L2 cache,則是主存。
圖3
當(dāng)說“invalidate icache to PoU”的時候,是指invalidate icache,使下次訪問時從L2 cache(PoU)讀取。
PoU的一個應(yīng)用場景是:運行的時候修改自身代碼之后,使用兩步來刷新cache,首先,clean dcache到PoU,然后invalidate icache到PoU。
Memory consistency
Armv8-A采用弱內(nèi)存模型,對normal memory的讀寫可能亂序執(zhí)行,頁表里可以配置為non-reordering(可用于 device memory)。
Normal memory:RAM、Flash、ROM in physical memory,這些內(nèi)存允許以弱內(nèi)存序的方式訪問,以提高性能。
單核單線程上連續(xù)的有依賴的str和ldr不會受到弱內(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)的其它觀測者在觀測到dmb之后的數(shù)據(jù)訪問之前觀測到dmb之前的數(shù)據(jù)訪問:
str x0, [x1]
dmb
str x2, [x3] //如果觀測者看到了這行str,則一定也可以看到第 1行str
同時,dmb還保證其后的所有數(shù)據(jù)訪問指令能看到它之前的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ù)訪問的結(jié)果。
等待當(dāng)前PE發(fā)起的所有cache、TLB、分支預(yù)測維護(hù)操作對指定的shareability domain可見。
可用于在sev指令之前保證數(shù)據(jù)同步。
一個例子:
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:
圖4
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.
圖5
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指令,可能被亂序執(zhí)行,有可能最終產(chǎn)生r1==r2==42的結(jié)果,即A看到了D,C看到了B。
典型的relaxed ordering的使用場景是簡單地增加一個計數(shù)器,例如std::shared_ptr中的引用計數(shù),只需要保證原子性,沒有memory order的要求。
Release-acquire
Rel-acq原子操作除了保證原子性,還保證使用release 的store和使用acquire的load之間的同步,acquire時必可以看到release之前的指令,release時必看不到 acquire之后的指令。
#include
#include
#include
#include
std::atomic
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,當(dāng)釋放鎖的時候,釋放之前的臨界區(qū)的內(nèi)存訪問必須都保證對同時獲取鎖的觀測者可見。
Release-consume
和rel-acq相似,但不保證consume之后的訪存不會在release之前完成,只保證consume之后對consume load操作有依賴的指令不會被提前,也就是說consume 之后不是臨界區(qū),而只是使用release之前訪存的結(jié)果。
Note that currently (2/2015) no known production compilers track dependency chains: consume operations are lifted to acquire operations.
#include
#include
#include
#include
std::atomic
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í)行,這時候不保證能看到release store,也就不保證能看到data=42。
Sequentially-consistent
Seq-cst ordering和rel-acq保證相似的內(nèi)存序,一個線程的seq-cst load如果看到了另一個線程的seq-cst store,則必可以看到store之前的指令,并且load之后的指令不會被store之前的指令看到,同時,seq-cst還保證每個線程看到的所有seq-cst指令有一個一致的total order。
典型的使用場景是多個producer多個consumer的情況,保證多個consumer能看到producer操作的一致total order。
#include
#include
#include
std::atomic
std::atomic
std::atomic
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的賦值順序,所以必至少有一個執(zhí)行到++z。
Seq-cst和其它ordering混用時可能出現(xiàn)不符合預(yù)期的結(jié)果,如下面例子中,對thread 1來說,A sequenced before B,但對別的線程來說,它們可能先看到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
編輯:黃飛
評論