现在的compiler与CPU为了最佳化执行效能,必要时可能重新安排执行程式的流程顺序。
1.compiler最佳化可依据CPU的instruction issue数目,执行的latency cycles以及程式流程,在不影响程式上下文结果下重排或简化程式。
2.硬件设计最佳化:
- multiple issue of instructions:一个cycle可以执行多条指令.
- out-of-order execution:如果某一条指令stall等待之前的结果时,CPU可以先执行下一条没有相依性的指令。
- speculation:当CPU遇到一些条件十判断的指令时,在判断出结果前可以先预测性的执行可能path以求得performance gain。
- speculative loads: 在cacheable region, load instrcution 真正被执行前就猜测性的先把资料读进cache。
- load and store optimizations:读写外部存储会花很长时间且可能stall pipline等待结果,CPU可尽量减少传输次数提高performance,例如合并数据相邻地址的store成一笔。
- external memory systems:在许多SOC系统中,有许多不同的master、slave,以及之间不同的routing。有些device同时接受来自不同的master的资料传输请求。这些传输的transaction有可能在interconnection之间被buffer或时recorder。
基于以上原因,你的程式的执行顺序与流程可能在编译时被重排或者修改,CPU执行结果的出现顺序又可能与assembly看到的不同,compiler跟CPU只会保证执行结果上下文时正确的。这在SMP环境下,其他CPU或是IO因为上述因素可能得到非预期的执行结果。我们需要在某些CPU/CPU或CPU/IO之间需要通过的地址确保compiler与CPU的执行顺序,memory barrier提供这样的功能。
compiler support
1.volatile
keyword:volatile是一个type qualifier。它声明锁修饰的变量的值有可能被memory-mapped IO或者是asynchronously interrupting function修改,这个关键字告诉compiler不要针对此变量的存取做最佳化。你可以对变量设定volatile,但是他对所有存取这个变量的地方都会造成效果。这造成能效的减损。Linux kernel里面提供ACCESS_ONCE()
macro在使用上进一步优化,只在需要的地方才套用volatile这个关键字,保留给programmer更多弹性:
#define ACCESS_ONCE(x) (*(volatile typeof(x)) &(x))
基本上就是在有需要阻止最佳化的地方通过类型转换来增加volatile修饰:
static int rcu_gp_in_progress(struct rcu_state *rsp)
{
return (ACCESS_ONCE(rsp->completed) != ACCESS_ONCE(rsp->gpnum));
}
表示我们希望读取rsp->completed
和rsp->gpnum
动作不要被最优化。
compiler barrier:compiler barrier 本身是一個 sequence point
int A, B;
void foo()
{
A = B + 1;
B = 0;
}
有可能在加了-O2
被 reorder 成
B = 0;
A = B + 1;
GCC 使用下列的 inline assembly 來表示 compiler barrier
asm volatile("" ::: "memory");
如同上面的程式碼改寫成以下的方式,我們可以確保編譯時期保持預期的順序。
A = B + 1;
asm volatile("" ::: "memory");
B = 0;
Linux kernel 在include/linux/compiler-gcc.h 中定義 compiler barrier macro barrier() 。
#define barrier() __asm__ __volatile__("": : :"memory")
CPU barrier
每个CPU architecture根据各自的memory model,通常会提供自己的barrier instruction,以达到不同程度的读写顺序的保证。如ARM dmb等,有的有不同程度与作用范围的barrier,以达到细读控制。
在 Linux 中,這些指令在 arch 下被包成通用的介面,分類介紹如下
-
mb()/rmb()/wmb()
:rmb() 確保 barrier 之前的 read operation 都能在 barrier 之後的 read operation 之前發生,簡單來說就是確保 barrier 前後的 read operation 的順序;wmb() 如同 rmb() 但是只針對 write operation。mb() 則是針對所有的 memory access。 -
smp_mb()/smp_rmb()/smp_wmb()
:在 SMP 的系統被定義成 mb()/rmb()/wmb() ,UP 時就只是 compiler barrier。可特別用在只於 SMP 時才需要 barrier 的地方[8]。 -
dma_rmb/dma_wmb()
:如果 architecture 對於 barrier 作用的範圍有提供更 fine-grained 的控制,在 device driver 中需要同步 CPU 與 IO 中的 memory data 時 我們就不需要使用作用達到整個系統的 barrier。 -
smp_load_acquire()/smp_store_release()
: 这部分单向的barrier。ACQUIRE确保之后所有memory operation都只在ACQUIRE之后出现;RLEASE则是确保之前的所有memory operation都在RELEASE之前出现。通常这两个承兑出现。通过两个marco,确保之前的critical section之内的变量存取都在这次critical section前完成。 -
read_barrier_depends()/smp_read_barrier_depends()
:只有在 barrier 上下的資料 存取有相依性時才有作用,這樣我們就可以避免使用 rmb() 達到更輕量的控制。但是這 只有 ALPHA CPU 才有支援,其他的 architecture 都是定義成空巨集。 -
smp_mb__before_atomic()/smp_mb__after_atomic()
:在某些沒有 return value 的 atomic operation 中有些沒有使用 memory barrier。這兩個 macro 讓我們在這些操作前 後確保資料一致性。
Barrier 在需要時幫助我們達到記憶體存取的順序的準確與可預測性。但是相對的它也減 低了原本效能最佳化的好處。它在其它 Concurrency 機制的實現上是個不可或缺的角色!