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è)者訪問(其它核、GPU、DMA 設(shè)備),inner和outer的區(qū)別是要求cache coherence的范圍,inner觀測(cè)者和outer觀測(cè)者的劃分是implementation defined。
圖1
PoC&PoU
當(dāng)clean或invalidate cache的時(shí)候,可以操作到特定的cache級(jí)別,具體地,可以到下面兩個(gè)“點(diǎn)”:
Point of coherency(PoC):保證所有能訪問內(nèi)存的觀測(cè)者(CPU 核、DSP、DMA 設(shè)備)能看到一個(gè)內(nèi)存地址的同一份拷貝的“點(diǎn)”,一般是主存。
圖2
Point of unification(PoU):保證一個(gè)核的icache、dcache、MMU(TLB)看到一個(gè)內(nèi)存地址的同一份拷貝的“點(diǎn)”,例如unified L2 cache是下圖中的核的PoU,如果沒有L2 cache,則是主存。
圖3
當(dāng)說“invalidate icache to PoU”的時(shí)候,是指invalidate icache,使下次訪問時(shí)從L2 cache(PoU)讀取。
PoU的一個(gè)應(yīng)用場(chǎng)景是:運(yùn)行的時(shí)候修改自身代碼之后,使用兩步來刷新cache,首先,clean dcache到PoU,然后invalidate icache到PoU。
Memory consistency
Armv8-A采用弱內(nèi)存模型,對(duì)normal memory的讀寫可能亂序執(zhí)行,頁(yè)表里可以配置為non-reordering(可用于 device memory)。
Normal memory:RAM、Flash、ROM in physical memory,這些內(nèi)存允許以弱內(nèi)存序的方式訪問,以提高性能。
單核單線程上連續(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ù)訪問之前觀測(cè)到dmb之前的數(shù)據(jù)訪問:
str x0, [x1]
dmb
str x2, [x3] //如果觀測(cè)者看到了這行str,則一定也可以看到第 1行str
同時(shí),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ù)測(cè)維護(hù)操作對(duì)指定的shareability domain可見。
可用于在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可以通過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原子操作只保證原子性,不保證同步語(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ù),只需要保證原子性,沒有memory order的要求。
Release-acquire
Rel-acq原子操作除了保證原子性,還保證使用release 的store和使用acquire的load之間的同步,acquire時(shí)必可以看到release之前的指令,release時(shí)必看不到 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這個(gè)寫操作。
這段代碼在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)存訪問必須都保證對(duì)同時(shí)獲取鎖的觀測(cè)者可見。
Release-consume
和rel-acq相似,但不保證consume之后的訪存不會(huì)在release之前完成,只保證consume之后對(duì)consume load操作有依賴的指令不會(huì)被提前,也就是說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í)行,這時(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
#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的賦值順序,所以必至少有一個(gè)執(zhí)行到++z。
Seq-cst和其它ordering混用時(shí)可能出現(xiàn)不符合預(yù)期的結(jié)果,如下面例子中,對(duì)thread 1來說,A sequenced before B,但對(duì)別的線程來說,它們可能先看到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)論