专栏首页蓝天C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI

C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI

1. 前言

本文内容主要针对Linux,而且主要是x86环境。先看一常见用法:

class Thread { public:     X()         : _stop(false) {     }          void stop() {         _stop = true;     }          void run() {         while (!_stop) {             work();         }     }      private:     volatile bool _stop; };

然后看看标准C++基金会https://isocpp.org)怎么说的(官方链接):

2. 结论

1) 与平台无关的多线程程序,volatile几乎无用(Java和C#中的volatile除外);

2) volatile不保证原子性(一般需使用CPU提供的LOCK指令);

3) volatile不保证执行顺序;

4) volatile不提供内存屏障(Memory Barrier)和内存栅栏(Memory Fence);

5) 多核环境中内存的可见性和CPU执行顺序不能通过volatile来保障,而是依赖于CPU内存屏障。

注:volatile诞生于单CPU核心时代,为保持兼容,一直只是针对编译器的,对CPU无影响。

volatile在C/C++中的作用:

1) 告诉编译器不要将定义的变量优化掉;

2) 告诉编译器总是从缓存取被修饰的变量的值,而不是寄存器取值。

就前言中的代码,可移植的实现方式为:

#include <atomic>   class Thread { public:     X()         : _stop(false) {     }          void stop() {         _stop = true;     }          void run() {         while (!_stop) {             work();         }     }      private:     std::atomic<bool> _stop; };

不过这要求至少C++11,否则可使用兼容C++98的实现CAtomic替代:

https://github.com/eyjian/libmooon/blob/master/include/mooon/sys/atomic.h,实际上Linux内核源代码都带有这些基础设施。

3. volatile应用场景

1) 信号处理程序;

2) 与硬件打交道(嵌入式开发用得多);

3) 和setjmp、longjmp配合(请参见:http://www.cplusplus.com/reference/csetjmp/setjmp/),原因同信号处理。

4. 内存屏障(Memory Barrier)

内存屏障,也叫内存栅栏(Memory Fence)。分编译器屏障(Compiler Barrier,也叫优化屏障)和CPU内存屏障,其中编译器屏障只对编译器有效,它们的定义如下表所示(限x86,其它架构并不相同):

#define barrier() \     __asm__ __volatile__("":::"memory")

编译器屏障,如果是GCC,则可用__sync_synchronize()替代

#define mb() \     __asm__ __volatile__("mfence":::"memory")

CPU内存屏障,还分读写内存屏障

#define rmb() \     __asm__ __volatile__("lfence":::"memory")

CPU读(Load)内存屏障

#define wmb() \     __asm__ __volatile__("sfence":::"memory")

CPU写(Store)内存屏障

x86架构的CPU内存屏障代码可在内核源代码的arch/x86/include/asm/barrier.h中找到。对于原子操作,需要使用CPU提供的“lock”指令,对于CPU乱序需使用CPU内存屏障。

代码顺序

编译器顺序

CPU顺序

a=1; b=x; c=z; d=2;

a=1; d=2; b=x; c=z;

b=x; c=z; a=1; d=2;

推荐资料:

https://mariadb.org/wp-content/uploads/2017/11/2017-11-Memory-barriers.pdf

内存屏障进一步还分读内存屏障和写内存屏障等,对非内核开发者来说,内存屏障的主要应用场景为无锁编程(Lock-free),因为像pthread_mutx_t等实际已经包含了“Lock”和“Memory Barrier”,所以无需再操心。

5. setjmp和longjmp

在C/C++中,goto关键词只能函数内的局部跳转,函数间的跳转需要使用setjmp和longjmp,这也是有些协程库基于setjmp和longjmp实现的原因。

1) setjmp

保存上下文,包括信号掩码,类似于setcontext。该函数的返回值比较特别,第一次返回0,第二次返回的longjmp第二个参数值(如果longjmp第二个参数值为0,则返回值为1,这样方便区分于第一次返回)。

2) longjmp

该函数从不返回,而是跳回到setjmp保存点,类似于swapcontext。如果没有先调用setjmp,则longjmp的行为是未定义的。C++代码可能还会执行栈展开(Unwinding),如果调用了任何非平凡析构函数(non-trivial destructors,需显示处理的析构函数,如内存释放),也会导致未定义的行为。

3) 代码示例(摘自http://www.cplusplus.com/reference/csetjmp/setjmp/

/* x.cpp */ /* setjmp example: error handling */ #include      /* printf, scanf */ #include     /* exit */ #include     /* jmp_buf, setjmp, longjmp */   struct X {   X() { fprintf(stderr, "X::ctor\n"); }   ~X() { fprintf(stderr, "X::dtor\n"); } };   int main() {   jmp_buf env;   int val = -1;   int m = -1;   volatile int n = -1;   X x;     val = setjmp(env);   fprintf(stderr, "setjmp return: %d\n", val);   if (val) {     fprintf(stderr, "m: %d\n", m);     fprintf(stderr, "n: %d\n", n);     exit(val);   }   else {     m = 2018;     n = 2018;       /* code here */     longjmp(env, 19);   /* signaling an error */     return 0;   } }

上例代码运行有两种输出结果:

1) 结果1(非优化编译:g++ -g -o x x.cpp -O0)

X::ctor setjmp return: 0 setjmp return: 19 m: 2018 n: 2018

非优先编译时,总是从内存取值。

2) 结果2(优化编译:g++ -g -o x x.cpp -O2)

X::ctor setjmp return: 0 setjmp return: 19 m: -1 n: 2018

因m未加volatile修饰,直接读取寄存器值,因此结果是-1。从这里也可以看出,即使是单线程程序,volatile也是必要的,也说明volatile并不是完全没用,只是它不能帮助解决多线程的原子性、内存屏障和CPU乱序执行。

另外可发现,上列代码的类X的析构未执行,但若将exit改成return,则会执行类X的析构,遇到“}”和“return”时,编译器会安插析构函数调用。

6. 不同CPU架构的一致性模型

注:LOAD操作,STORE操作。

Loads reordered after loads

Loads reordered after stores

Stores reordered after stores

Stores reordered after loads

Atomic reordered with loads

Atomic reordered with stores

Dependent loads reordered

Incoherent instruction cache pipeline

Alpha

Y

Y

Y

Y

Y

Y

Y

Y

ARMv7

Y

Y

Y

Y

Y

Y

Y

PA-RISC

Y

Y

Y

Y

POWER

Y

Y

Y

Y

Y

Y

Y

SPARC RMO

Y

Y

Y

Y

Y

Y

Y

SPARC PSO

Y

Y

Y

Y

SPARC TSO

Y

Y

x86

Y

Y

x86 oostore

Y

Y

Y

Y

Y

AMD64

Y

IA-64

Y

Y

Y

Y

Y

Y

Y

z/Architecture

Y

四种SMP架构的CPU内存一致性模型:

1) 顺序一致性模型(SC,Sequential Consistency,所有读取和所有写入都是有序的);

2) 宽松一致性模型(RC,Relaxed Consistency,允许某些可以重排序),ARM和POWER属于这类;

3) 弱一致性模型(WC,Weak Consistency,读取和写入任意重新排序,仅受显式内存屏障限制);

4) 完全存储排序(TSO,Total Store Ordering),SPARC和x86属于这种类型,只有“store load”一种情况会发生重排序,其它情况和SC模型一样。

7. x86-TSO

x86-TSO是Intel推出的一种CPU内存一致性模型,特点是只有“Store Load”一种情况会重排序,也就是“Load”可以排(乱序)在“Store”的前面,因此不需要“Load Load”、“Store Store”和“Load Store”这三种屏障。

1) “Store Load”屏障的作用是:确保“前者刷入内存”的数据对“后者加载数据”是可见的;

2) “Load Load”屏障的作用是:确保“前者装载数据”先于“后者装载指令”;

3) “Store Store”屏障的作用是:确保“前者数据”先于“后者数据”刷入内存,且“前者刷入内存的数据”对“后者是可见的”;

4) “Load Store”屏障的作用是:确保“前者装载数据”先于“后者刷新数据到内存”。

8. C++标准库对内存顺的支持

1) 头文件

enum memory_order {     memory_order_relaxed, // 宽松一致性模型,不对执行顺序做任何保证     memory_order_consume, // (读操作)本线程所有后续有关本操作的必须在本操作完成后执行     memory_order_acquire, // (读操作)本线程所有后续的读操作必须在本条操作完成才能执行     memory_order_release, // (写操作)本线程所有之前的写操作完成后才执行本操作     memory_order_acq_rel, // (读-修改-写)同时包含Acquire和Release     memory_order_seq_cst  // (读-修改-写,默认类型)顺序一致性模型,全部顺序执行 };

2) 头文件

// 默认内存顺类型为“memory_order_seq_cst” std::atomic std::atomic std::atomic 。。。。。。

附1:CPU、缓存和主存

第三级缓存(L3 Cache)多核共享:

附2:SMP对称多处理器结构

多个CPU对称工作没有区别,无主次或从属关系,平等地访问内存、外设和一个操作系统,共享全部资源,如总线、内存和I/O系统等,因此也被称为一致存储器访问结构(UMA : Uniform Memory Access)。

其它的架构有:

1) NUMA(Non-Uniform Memory Access,非统一内存访问),基本特征是将CPU分成多个模型,每个模型多个CPU组成,具有独立的本地内存和I/O槽口等;

2) MPP(Massive Parallel Processing,海量并行处理结构),基本特征是由多个SMP服务器(每个SMP服务器称节点)通过节点互联网络连接而成,每个节点只访问自己的本地资源(内存、存储等),是一种完全无共享(Share Nothing)结构。

附3:在线C++编译器

1) https://www.tutorialspoint.com/compile_cpp_online.php

2) https://www.jdoodlecom/online-compiler-c++

3) http://coliru.stacked-crooked.com/

4) https://www.onlinegdb.com/online_c++_compiler

5) https://ideone.com/

6) http://cpp.sh/

7) https://www.cppbuzz.com/compiler/online-c++-compiler

8) https://www.codechef.com/ide/

9) https://repl.it/repls/ZestyNaturalMatch

10) https://www.codiva.io

11) http://codepad.org/

12) https://cppinsights.io/

13) https://tio.run/#cpp-gcc

14) http://www.compileonline.com/

聚合了各种语言在线编译。

15) https://rextester.com/l/cpp_online_compiler_gcc

还支持其它众多语言在线编译。

附4:资源链接

1) C++标准委员会(The C++ Standards Committee)

http://www.open-std.org/jtc1/sc22/wg21/

2) 标准C++基金会

https://isocpp.org

3) C++之父

http://www.stroustrup.com/

4) Linux内核关于volatile的说明

https://www.kernel.org/doc/html/latest/process/volatile-considered-harmful.html

5) Intel内存模型(Intel Memory Model)

https://en.wikipedia.org/wiki/Intel_Memory_Model

6) Intel TSO内存模型

https://www.cl.cam.ac.uk/~pes20/weakmemory/x86tso-paper.tphols.pdf(A Better x86 Memory Model: x86-TSO)

http://homepages.inf.ed.ac.uk/vnagaraj/papers/hpca14.pdf(TSO-CC: Consistency directed cache coherence for TSO)

7) Sequential Consistency &TSO

https://www.cis.upenn.edu/~devietti/classes/cis601-spring2016/sc_tso.pdf

8) Write buffer

https://en.wikipedia.org/wiki/Write_buffer

9) x86-64和IA-32开发手册

https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html(IA-32:Intel Architecture 32-bit,即32位x86)

10) 编译器屏障(Compiler Barriers)

https://www.oracle.com/technetwork/server-storage/solarisstudio/documentation/oss-compiler-barriers-176055.pdf

11) C ++ 11中memory_order_consume的作用

https://preshing.com/20140709/the-purpose-of-memory_order_consume-in-cpp11/

12) MESI(多核CPU缓存一致性协议)

https://en.wikipedia.org/wiki/MESI_protocol

13) MESIF(多核CPU缓存一致性协议)

https://en.wikipedia.org/wiki/MESIF_protocol

M 修改(Modified)

该Cache line(缓存行)有效,数据被修改(dirty)了,和主存中的数据不一致,数据只存在于本Cache中

E 独占互斥(Exclusive)

该Cache line只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致

S 共享(Shared)

该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中

I 无效(Invalid)

该Cache line无效,可能有其它CPU修改了该Cache line

F 转发(Forward)

Intel提出来的,意思是一个CPU修改数据后,直接针修改的结果转发给其它CPU

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • C/C++常见错误汇总

    出错原因: main.cpp中没有找到对应的函数名声明,没有在.cpp引用包含该函数名的头文件.h。 解决方法: 引入对应头文件。

    城市中的游牧民族
  • Lua和C如何交互(一)

    全局表中存放了name和hello world的对应关系, 可以通过name在全局表中找到对应的hello world

    程序手艺人
  • 从公有云上导镜像导私有云环境

    要求环境上的机器有访问公网的能力,同时安装的docker版本最低为1.12.6,推荐使用的版本为17.12.1(或者以上)

    用户1502070
  • C#脚本实践(三): 集成到游戏

    至此, C#做为脚本已经完全可行了: 可嵌入, 跨平台, 高效率, 热更新, 几乎可以忽略的编译时间, 强大的IDE支持, 丰富的第三方库, 部分动态语言特性的...

    逍遥剑客
  • 关于Web3D

    逍遥剑客
  • 修复miniblink 文件编码检测和退出内存泄漏的bug

    文本检测的bug原因是我把icu整个都端了,自然icu里检测编码的好用接口也废弃了。不过我扣了一部分出来,用于检测UTF8和GBK编码。剩下的编码,经海绵宝宝

    龙泉寺扫地僧
  • 2018年的经历有苦有甜

    2018年每周的任务,目标都通过Trello一直记录着,看板的形式非常直观,目标清晰,同时也能看到自己在某一方面的时间投入和支出。这也是工作之后一直坚持使用...

    程序手艺人
  • JNI入门

    首先声明一点,Eclipse不仅仅适合于开发Java应用程序。Eclilpse为Java开发提供了极好的支持,它的设计初衷也是为了Java。但得益于它的设计架构...

    刘晓杰
  • C++ list

    [work@db-testing-com06-vm3.db01.baidu.com c++]$ g++ -o list list.cpp 

    阳光岛主
  • 命名空间及模块化

    Java用package来达到命名空间同样的效果。 C++就有对应关键字:using namespace。 JavaScript库用对象来封装命名空间。

    城市中的游牧民族

扫码关注云+社区

领取腾讯云代金券