Linux concurrency - 2.barrier

现在的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->completedrsp->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 機制的實現上是個不可或缺的角色!

上一篇:MySQL引擎 InnoDB、MyISAM、MEMORY


下一篇:【Bugs】RuntimeError CUDA out of memory