代码优化和内存屏障(barrier、mb、rmb、wmb)

内存屏障用于解决编译器优化和CPU乱序执行导致的执行顺序问题。文章介绍了Linux下barrier()、mb()、rmb()、wmb()宏的作用,它们通过volatile和特定汇编指令确保内存访问顺序,以保证并发环境中的数据一致性。在现代编译器和CPU追求性能优化的背景下,理解并适当使用内存屏障至关重要。
部署运行你感兴趣的模型镜像

    内存屏障主要解决的问题是编译器的优化和CPU的乱序执行。
    编译器在优化的时候,生成的汇编指令可能和c语言程序的执行顺序不一样,在需要程序严格按照c语言顺序执行时,需要显式的告诉编译不需要优化,这在linux下是通过barrier()宏完成的,它依靠volatile关键字和memory关键字:

  1. /* Optimization barrier */
    /* The "volatile" is due to gcc bugs */
    #define barrier() __asm__ __volatile__("": : :"memory")
     
  2. volatile: 告诉编译barrier()周围的指令不要被优化;
  3. memory:是告诉编译器汇编代码会使内存里面的值更改,编译器应使用内存里的新值而非寄存器里保存的老值。

    同样,CPU执行会通过乱序以提高性能。汇编里的指令不一定是按照我们看到的顺序执行的。linux中通过mb()系列宏来保证执行的顺序。具体做法是通过mfence/lfence指令(它们是奔4后引进的,早期x86没有)以及x86指令中带有串行特性的指令(这样的指令很多,例如linux中实现时用到的lock指令,I/O指令,操作控制寄存器、系统寄存器、调试寄存器的指令、iret指令等等)。简单的说,如果在程序某处插入了mb()/rmb()/wmb()宏,则宏之前的程序保证比宏之后的程序先执行,从而实现串行化。wmb的实现和barrier()类似,是因为在x86平台上,写内存的操作不会被乱序执行。
    实际上在RSIC平台上,这些串行工作都有专门的指令由程序员显式的完成,比如在需要的地方调用串行指令,而不像x86上有这么多隐性的带有串行特性指令(例如lock指令)。所以在risc平台下工作的朋友通常对串行化操作理解的容易些。


更详细的介绍请参考: http://vh21.github.io/linux/2015/06/22/concurrency-in-the-linux-kernel-2.html

現在的 Compiler 與 CPU 為了最佳化執行效能,必要時可能重新安排執行程式的流程與順 序。從 [1] 中我們可以大略認識幾個可能造成存取亂序的軟硬體最佳化,

  1. Compiler 最佳化 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 instruction 真正被執行前就猜測 性的先把資料讀進 cache。
    • Load and store optimizations: 讀寫外部記憶體會花上很長的時間且可能 stall pipeline 等待結果,CPU 可藉由減少傳輸次數提高 performance,例如合併數筆相鄰 位址的 store 成一筆。
    • External memory systems:在許多現今複雜的 SOC 系統中,有許多不同的 master、 slave、與以及之間不同的 routing。有些 device 同時接受來自不同 master 的資料 傳輸請求。這些傳輸的 transaction 有可能在些 interconnection 之間被 buffer 或是 reorder。

基於以上原因,你的程式的執行順序與流程可能在編譯時被重排或者修改,CPU 執行結果 的出現順序又可能與 assembly 看到的不同, Compiler 跟 CPU 只會保證執行上下文結果 是正確的。這在 SMP 環境下,其他 CPU 或是 IO 因為上述因素有可能得到非預期的執行 結果。我們需要在某些 CPU/CPU 或是 CPU/IO 之間需要溝通的地方[2]確保 Compiler 與 CPU 的執行順序,memory barrier 提供這樣的功能。

Compiler support

    volatile keyword:volatile 是一個 type qualifier。它聲明所修飾的變數的值有可 能被 memory-mapped IO 或者是 asynchronously interrupting function 修改,這個關 鍵字告訴 compiler 不要針對此變數的存取做最佳化。你可以對變數設定 volatile,但是 他對於所有存取這個變數的地方都會造成效果。這造成效能的減損。Linux kernel 裡面提 供 ACCESS_ONCE() macro [3]在使用上進一步最佳化,只在有需要的地方才套用 volatile 這個關鍵字,保留給 programmer 更多彈性。以下是 linux-3.19 前的實現版 本.

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。如同 [9] 中 ARM 的例子,

  • smp_load_acquire()/smp_store_release():這部分是單向的 barrier。 ACQUIRE 確保 之後所有的 memory operation 都只在 ACQUIRE 之後出現;RELEASE 則是確保之前所有的 memory operation 都在 RELEASE 之前出現。通常這兩個都是成對出現。透過這兩個 macro,我們可以確保之前在 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 機制的實現上是個不可或缺的角色!

Reference

  1. Chapter 13 Memory ordering, ARM Cortex-A Series Programmer’s Guide for ARMv8-A
  2. Sequence point
  3. LINUX KERNEL MEMORY BARRIERS
  4. Memory Ordering at Compile Time
  5. C語言: 認識關鍵字volatile
  6. How To Add a Sequence Point?
  7. Memory ordering
  8. Ordering and Barriers, Linux kernel development
  9. [PATCH v6 2/5] arch: Add lightweight memory barriers dma_rmb() and dma_wmb()

您可能感兴趣的与本文相关的镜像

LobeChat

LobeChat

AI应用

LobeChat 是一个开源、高性能的聊天机器人框架。支持语音合成、多模态和可扩展插件系统。支持一键式免费部署私人ChatGPT/LLM 网络应用程序。

<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()`。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值