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。
图1
PoC&PoU
当clean或invalidate cache的时候,可以操作到特定的cache级别,具体地,可以到下面两个“点”:
Point of coherency(PoC):保证所有能访问内存的观测者(CPU 核、DSP、DMA 设备)能看到一个内存地址的同一份拷贝的“点”,一般是主存。
图2
Point of unification(PoU):保证一个核的icache、dcache、MMU(TLB)看到一个内存地址的同一份拷贝的“点”,例如unified L2 cache是下图中的核的PoU,如果没有L2 cache,则是主存。
图3
当说“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:
图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指令,可能被乱序执行,有可能最终产生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
#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,当释放锁的时候,释放之前的临界区的内存访问必须都保证对同时获取锁的观测者可见。
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
#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到非空指针之前执行,这时候不保证能看到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
#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的赋值顺序,所以必至少有一个执行到++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
编辑:黄飞
评论
查看更多