memory barrier

本文探讨了CPU指令乱序执行的原因及其实现方法,分析了乱序执行带来的性能提升和潜在副作用,特别是在多核环境下。介绍了内存屏障(memorybarrier)的作用,包括其在不同体系结构下的表现和如何通过编程手段确保数据的一致性和顺序性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、概述

二、指令乱序

2.1 为什么要乱序

2.2 如何实现乱序

2.3 乱序执行带来的副作用

三、memory barrier

3.1 memory consistency model

3.2 memory barrier

3.3 锁

参考


一、概述

memory barrier 产生的原因要深入理解体系结构,这篇文字有点敷于表面,记录一个分析的思路

二、指令乱序

2.1 为什么要乱序

在按照program order执行命令的cpu上,考虑到程序之间有的指令存在指令相关,这种指令的相关性会造成性能的下降,考虑下面的指令:

1.   load  r1, (r0)             // r1 = [r0]
2.   add   r1,  r2              // r2 = r1 + r2
3.   store r3, (r4)             // [r4] = r3

在顺序执行条件下,假如指令1的load指令产生了cache miss,由于指令1,2是相关的,2需要等待1的结果(r1),这会造成流水线stall,尽管3指令和1,2都不相关,但是由于顺序执行的要求,这里也无法得到执行。

考虑这样一种方式,我们通过指令调度,将3在安排在1后执行,这样等到2开始执行时,1中r1的结果已经可用了,这样就可以掩盖流水线的stall,提高指令执行效率。这样的执行行为其实打破了顺序执行的规则,事实上指令进行了乱序执行。

所以,乱序执行可以提高性能。

2.2 如何实现乱序

          可以通过两个方法实现指令乱序:

  1. 利用编译器实现(compile time),称为指令的静态调度
  2. 利用硬件(runtime),称为动态调度

一般的,编译器乱序是可以观察到的——通过对比不同优化等级的汇编指令可以看出。而硬件的乱序执行我们无法直观的看到,这些可以通过了解一些微架构的实现理解其原理。

2.3 乱序执行带来的副作用

尽管乱序执行可以带来性能上的提升,但在某些情况下,尤其是多核下,会造成一些问题,考虑如下代码:

void foo(void)
{
    a = 1;
    b = 1;
}

void bar(void)
{
    while (b == 0) continue;
    assert(a == 1);
}

假如foo在cpu0执行,bar在cpu1执行,在cpu0看来a = 1, b = 1指令是无关的,因此foo在执行时有可能先执行b=1,再执行a=1。然而我们看到在bar中,如果前面所说的顺序执行,assert会失败。失败的原因在于a,b没有按照program order执行,编译器和cpu没有看到bar中a,b中逻辑关系的能力,这时候就需要程序员通过显式的指定a,b执行的顺序,即memory barrier。

三、memory barrier

本节主要针对的是内存操作——load/store乱序说明,两个操作数都是寄存器的指令不在考虑范围内。

3.1 memory consistency model

简称memory model,在我们只考虑load,store两种执行的情况下,乱序的情况只有以下四种:

不同体系结构通过允许上述四种乱序中的某几种来构成自己的memory model,如在x86中,只允许store load乱序,不允许其他形式的乱序(实际上x86在一些情况下也存在store-store order Does it make any sense to use the LFENCE instruction on x86/x86_64 processors?,lfence没有作用,sfence代表store-store barrier,mfence代表full barrier,sfence+lfence也可以是full barrier)。

3.2 memory barrier

我们可以通过memory barrier指示编译器或者cpu不对指令进行乱序,如两条不相关的load store指令:

load

load-store-barrier

store

由于每种cpu的memory model不同,它提供的memory barrier原句也不同,可能一条指令可以是避免上述一种或者几种乱序的组合。如读屏障一般是load-load barrier 和load-store barrier组合:

  • read barriers             load-load barrier
  • write barriers            store-store barrier
  • acquire barriers        load-load barrier + load-store barrier
  • release barriers        load-store barrier + store-store barrier
  • full barrier                 all 4,so acquire + release  != full

3.3 锁

我们一般使用锁的方式为:

lock

critical section

unlock

那么我们考虑,在乱序执行的条件下,锁是如何避免critical section的语句没有提前到lock之前,也没有滞后unlock执行的呢?

事实上,在memory model种有Acquire和Release语义的概念。

Acquire是说,在Acquire之后的读写指令都不能和Acquire之前的read指令乱序

Release是说,在Release之后的write指令都不能和Release之前的读写指令乱序

这样,如果一个锁的实现保证lock aquire 和unlock release的,那么我们上面提到的乱序执行就不会发生:

那么如何保证呢?使用memory barrier,我们看下图:

即使用load-load barrier和load-store barrier保证Lock Acquire,使用load-store barrier和store-store barrier 保证Unlock Release。

可以看到没有用到store-load barrier,而x86刚好只有store load乱序,因此cpu天然为我们保证了Lock Acquire和Unlock Release。

 

参考

【1】Is Parallel Programming Hard, And, If So, What Can You Do About It?

【2】Memory Barriers Are Like Source Control Operations

【3】Acquire and Release Semantics

【4】并发编程系列之一:锁的意义

 

 

 

 

 

 

 

 

 

 

 

<think>嗯,用户这次问的是内存屏障的作用,看来是在深入理解计算机底层机制。之前连续讨论了AUTOSAR和内存泄漏,现在转向系统级编程的并发控制,说明用户可能是嵌入式开发者或系统程序员,正在排查多线程环境下的同步问题。 内存屏障这个概念确实容易让人困惑,需要从硬件和编译器两个层面解释清楚。用户可能实际遇到过因内存乱序导致的诡异bug,比如数据明明更新了却读到旧值。我得用汽车装配线的例子类比CPU流水线,这样更直观。 硬件优化带来的指令重排是问题的核心。现代CPU为了效率会乱序执行,比如Store Buffer的存在可能让写操作延迟,导致其他核看到操作的顺序和代码顺序不一致。内存屏障就是强行给这些“调皮”的优化踩刹车,告诉CPU“到此为止之前的操作必须完成”。 编译器优化也是隐患。比如用volatile只能阻止编译器重排,但管不了CPU层面的乱序。这点在无锁编程中特别致命,用户可能正为此头疼。需要强调屏障是软硬双管齐下的解决方案。 用户应该还想知道具体使用场景,所以示例代码里要展示经典的双检锁模式。这个模式如果漏掉屏障,单例可能被初始化两次——这种bug极难复现但危害巨大。另外内核态的barrier()宏和用户态的__sync_synchronize()区别也得提一下。 最后的提问设计要层层递进:从原理(为什么需要)到实践(怎么用),再到特定领域(Linux内核)和替代方案。第五个问题特意问编译器屏障,因为很多人会混淆它与硬件屏障的作用范围。</think>内存屏障(Memory Barrier)的主要作用是**强制保证内存操作的顺序性和可见性**,解决多核/多线程环境下因**指令重排序**和**缓存一致性**引发的数据一致性问题。其核心作用如下: --- ### 核心作用 1. **禁止指令重排序** - 编译器和CPU可能为优化性能而乱序执行指令(如写操作延迟、读操作提前)。 - 内存屏障会阻止屏障前后的指令跨越屏障执行,确保程序逻辑顺序。 2. **保证内存可见性** - 强制将缓存数据刷新到主存(写屏障),或从主存加载最新数据(读屏障)。 - 确保一个线程的修改对其他线程立即可见。 3. **同步多核数据一致性** - 在弱一致性内存模型中(如ARM、PowerPC),屏障触发缓存失效/更新,维持多核缓存一致。 --- ### 类型与场景 | 屏障类型 | 作用 | 典型场景 | |-------------------|----------------------------------------------------------------------|--------------------------| | **写屏障(Store Barrier)** | 确保屏障前的所有写操作完成,数据刷入主存后才执行后续操作 | 修改共享数据后释放锁 | | **读屏障(Load Barrier)** | 确保后续读操作前,先加载主存最新数据(清空本地缓存) | 获取锁后读取共享数据 | | **全屏障(Full Barrier)** | 同时具备读写屏障功能(如 `mfence`),确保屏障前后指令无重排且全局可见 | 无锁数据结构、内核同步原语 | --- ### 代码示例(无锁编程中的屏障) ```c // 共享数据 int data = 0; bool ready = false; // 线程A:写入数据 void thread_A() { data = 42; // 写操作 // 写屏障:确保data写入完成后再更新ready __sync_synchronize(); // GCC内置全屏障 ready = true; // 标志位更新 } // 线程B:读取数据 void thread_B() { while (!ready); // 等待ready为true // 读屏障:确保读取data前获取最新值 __sync_synchronize(); printf("%d", data); // 必须输出42而非0或随机值 } ``` > **关键点**:若省略屏障,线程B可能因重排序先看到 `ready=true` 后看到 `data=0`(脏读)。 --- ### 典型应用场景 - **多线程同步**:锁实现(如自旋锁)、条件变量。 - **无锁数据结构**:CAS(Compare-And-Swap)操作前后插入屏障。 - **设备驱动**:确保硬件寄存器按顺序写入。 - **内核同步原语**:如Linux的 `smp_mb()`、`rmb()`、`wmb()`。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值