程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

内存屏障memory barrier, volatile, atomic(memory order)作用与区别

balukai 2025-05-14 11:54:50 文章精选 1 ℃

首先这三种技术的出现场景均是为了避免内存访问过程中出现一些不符合预期的行为。他们的作用有相似之处,但也有不同的细分场景,可以通过下面这张表先来做个简单总结



抑制编译器重排

比如我们有如下代码


编译器在生成目标代码过程中发现上面的两行代码彼此之间没有关系,因此编译器不保证汇编代码中p_a的写入一定在p_b读取之前。如果这个顺序对你来说是有意义的,你可以通过一些手段来防止编译器做重排

  • 把对应的变量设置为volatile, C++保证对volatile变量之间的访问不会做重排,仅仅是volatile变量之间,但是不保证volatile变量与其他变量之间
  • 在需要的地方手动添加合适的memory barrier(内存栅栏), 他可以保证编译器不会进行错误重排
  • 把对应的变量声明为atomic, 他的作用和volatile类似,C++也保证atomic变量之间的访问不会进行重排


抑制编译器优化

主要指编译器不生成他自己认为无意义的内存访问代码,比如下面


在这里,a这个变量似乎不会起任何效果,因此对a的内存访问都会被优化掉。在这种情况下,f()生成汇编会跟空函数差不多。但是如果此时需要对a循环若干次,我们就需要通过一些行为来防止编译器进行优化。

  • 可以对变量声明为volatile / atomic, C++保证对这种变量的访问肯定会发生,因此不会被优化掉

需要注意的是,针对这种case, 手动添加内存屏障mfence是没有意义的,因为他仅仅是让循环不被优化,但是内部对a的访问仍然会被优化掉


抑制CPU乱序

上面说到了编译器重排,但是要记住一点的就是,即使没有编译器重排,内存访问也不一定会按照我们代码中的顺序进行执行。因为现代CPU有诸多特性会影响这个行为,对应不同架构的CPU来说,其所保证的内存存储模型是不一样的。比如x86_64就会所谓的TSO(完全存储定序)模型,而很多的ARM则是RMO(宽松存储模型),再加上多核间Cache一致性问题,多线程编程会面临更多的挑战。


为了解决这些问题,要从根本上来通过插入Memory Barrier内存屏障指令来解决,这些会指令会让CPU保持特定的内存访问顺序和内存写入操作系统在多核间的可见性。然而由于不同处理器架构件的内存模型和具体Memory Barrier指令均不同,需要在什么位置添加什么指令并不具有通用性。因此C++ 11在此基础上做了一层抽象,引入了atomic类型以及Memory Order的概念,有助于写出更通用的代码。从本质上atomic的memory order看就是编译器来帮我们根据代码中的更高层次的Memory Order来自动选择插入特定处理器的内存屏障指令


保证访问原子性

所谓访问原子性就是Read / Write是否存在中间状态。具体如何实现原子性的访问跟处理器的指令集有很大的关系。如果处理器本身就支持这些原子操作,比如Atomic Store, Atomic Load, Atomic Fetch Add, Atomic Compare And Swap(CAS), 那只需要在代码生成时选择合适的指令即可,否则就需要依赖锁来帮助解决。C++提供的这些可移植的通用方法其实就是std::atomic。volatile以及Memory Barrier与此都无关


总结

从上面的比较中可以看出,volatile, atomic, Memory Barrier的范围还是比较区分的:

  • 如果需要原子性的访问支持,只能选择atomic
  • 如果仅仅只是需要保证内存访问不会被编译器优化掉,优先考虑volatile
  • 如果需要保证memory order, 也可以考虑使用atomic, 只有当不需要保证原子性,而且需要明确要在哪里选择插入内存屏障的时候才考虑手动插入Memroy Barrier.

Tags:

最近发表
标签列表