指令重排和优化屏障

1. 优化带来的烦恼

用过GCC编译的同学应该知道GCC有O0、O1、O2、O3等优化选项,启用这些选项往往可以提高程序的运行效率,但它并不是万无一失的,尤其是在多线程场景下。而这些优化背后的技术正是指令重排。因为编译器或处理器也很难确定代码逻辑的原本意图。

锁能够保持原子性,但是经过编译器优化之后的代码,并不是绝对时序正确的,况且处理器还有可能进一步优化。这里面最经典的一个例子就是单例模式,Double-Checked Locking is Fixed In C++11

2. 内核提供的解决方案

内核提供以下方法,阻止编译器和处理器进行指令重排

  • mb() rmb() wmb() 会将硬件内存屏障插入到代码中。rmb用于读访问内存屏障,wmb用于写访问屏障,mb兼具二者。读屏障插入到代码中之后,保证屏障之前的读操作代码结束之后,屏障之后的读操作代码才读。
  • barrier 插入优化屏障。屏障之前所有有效的内存地址,在屏障之后都将失效。也就是屏障之后,不能再读写了。
  • smb_mb() smb_rmb() smb_wmb() 在SMP系统产生硬件内存屏障,如果用在单处理器系统上产生的将是软件屏障。
  • read_barrier_depends 会考虑读操作之间的依赖性,设置读访问屏障。

屏障肯定是会影响性能的,但总不能为了优化性能而让程序出现错误。在任何程序面前,正确性永远是第一位的。

说明两个概念

内存屏障:rmb(), wmb(), mb(),可以防止硬件上的指令重排,针对的是处理器CPU。

优化屏障:barrier(),避免编译器对内存访问的优化,针对的是编译器。

它们在很多地方称为内存栅栏,但英文的话都是memory barrier。

3. 典型的应用

3.1 安全的单例模式

Double-Checked Locking 咋一眼看山去没什么问题,但是考虑下指令重排,就会发现问题。如果返回指针在new之前,那是我们不愿看到的,而这里使用屏障可以解决这个问题。

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance;
    ...// insert memory barrier
    if (tmp == NULL) {
        Lock lock;
        tmp = m_instance;
        if (tmp == NULL) {
            tmp = new Singleton;
            ...// insert memory barrier
            m_instance = tmp;
        }
    }
    return tmp;
}

3.2 内核抢占

preempt_disable();
do_something();
preempt_enable();

在内核抢*,preempt_disable对计数器加1,也就是告诉其他线程,不要来抢这个是我的, preempt_enable反之。

#define preempt_disable() \
do { \
    inc_preempt_count(); \
    barrier();  \
} while(0)

 

#define preempt_enable() \
do { \
     barrier();  \
    preempt_check_resched(); \
} while(0)

 

 

参考:

[0] 深入Linux内核架构

[1] https://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/

 

上一篇:洛谷 P1032 字串变换


下一篇:checkPoint机制