指令重排和优化屏障

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/

 

<think>嗯,用户问的是内存屏障如何防止指令重排。首先,我需要回忆一下计算机组成原理操作系统的相关知识。记得指令重排是为了优化性能,但有时候会导致多线程下的问题。内存屏障的作用应该这个有关。 接下来,我需要确定用户的理解水平。他们可能对底层机制不太熟悉,所以得用比较易懂的例子来解释。比如用生产者消费者的场景,或者简单的变量读写来演示问题。 然后,内存屏障具体是怎么工作的呢?可能分为读屏障屏障,或者全屏障。需要说明不同类型的屏障如何限制重排的方向,比如后面的指令不能越过屏障提前执行,或者前面的不能延后。 还要考虑内存屏障在不同层面的应用,比如编译器在生成代码时可能重排指令,而CPU在执行时也会重排内存屏障需要在这两个层面都起作用,确保指令顺序符合预期。 另外,用户可能对为什么需要防止重排有疑问。应该举例说明如果没有屏障,可能出现数据竞争、状态不一致等问题,比如双重检查锁定中的单例模式失效。 需要注意的是,不能把内存屏障原子操作混为一谈,虽然它们都用于并发控制,但机制不同。内存屏障主要是顺序保证,而原子操作关注操作的不可分割性。 可能还需要提到一些实际的编程例子,比如在Java中使用volatile变量会自动插入内存屏障,或者在C++中使用特定的内存序参数。 最后,总结内存屏障的重要性,强调它在多线程环境下确保可见性顺序性的作用,避免因为优化带来的意外结果。</think>内存屏障(Memory Barrier)防止指令重排的含义,可以用一个简单的**生产-消费模型**来理解: 假设存在以下代码顺序: 1. 生产者准备数据(data = 123) 2. 设置就绪标志(ready = true) 3. 消费者检测到ready为true后读取data 💥**没有内存屏障时**: 编译器/CPU可能将指令重排为: 1. 先设置ready = true 2. 再准备data = 123 这会导致消费者看到ready=true时,data还没有准备好! 🔒**插入内存屏障后**: ```cpp // 生产者 data = 123; // 写操作 std::atomic_thread_fence(std::memory_order_release); // 写屏障 ready = true; // 标志位写入 // 消费者 while(!ready); // 等待标志 std::atomic_thread_fence(std::memory_order_acquire); // 读屏障 int val = data; // 安全读取 ``` 此时屏障确保: 1. 屏障前的所有写操作(data=123)必须完成 2. 屏障后的操作(ready=true)不能提前到屏障前 3. 消费者读取数据时,必须刷新内存视图 📌内存屏障的三种主要类型: 1. **写屏障**(Store Barrier):保证屏障前的所有写操作先于屏障后的写操作完成 2. **读屏障**(Load Barrier):保证屏障后的读操作不会重排屏障前 3. **全屏障**(Full Barrier):同时具备读+写屏障功能 💡实际应用场景: - 无锁数据结构实现 - 双重检查锁定模式 - 跨核共享数据同步 - 硬件寄存器操作 通过这种强制顺序保证,既保持了CPU/编译器的优化能力,又确保了关键操作顺序的正确性。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值