聊聊内存模型和内存序

本文深入探讨了C++内存模型,重点讲解了MemoryOrder及其在多线程环境中的作用。内存模型用于解决编译器优化、CPU乱序执行和缓存一致性带来的问题。文章介绍了sequenced-before和happens-before关系,以及内存约束符如memory_order_relaxed、memory_order_acquire、memory_order_release和memory_order_seq_cst等,帮助开发者理解如何在不使用锁的情况下保证数据一致性。

本文始发于公众号【高性能架构探索】,本公众号致力于分享干货、硬货以及工作上的bug分析,欢迎关注。回复【pdf】免费获取计算机经典书籍

你好,我是雨乐!

最近群里聊到了Memory Order相关知识,恰好自己对这块的理解是模糊的、无序的,所以借助本文,重新整理下相关知识。

写在前面

在真正了解Memory Model的作用之前,曾经简单地将Memory Order等同于mutex和atomic来进行线程间数据同步,或者用来限制线程间的执行顺序,其实这是一个错误的理解。直到后来仔细研究了Memory Order之后,才发现无论是功能还是原理,Memory Order与他们都不是同一件事。实际上,Memory Order是用来用来约束同一个线程内的内存访问排序方式的,虽然同一个线程内的代码顺序重排不会影响本线程的执行结果(如果结果都不一致,那么重排就没有意义了),但是在多线程环境下,重排造成的数据访问顺序变化会影响其它线程的访问结果。

正是基于以上原因,引入了内存模型。C++的内存模型解决的问题是如何合理地限制单一线程中的代码执行顺序,使得在不使用锁的情况下,既能最大化利用CPU的计算能力,又能保证多线程环境下不会出现逻辑错误。

指令乱序

现在的CPU都采用的是多核、多线程技术用以提升计算能力;采用乱序执行、流水线、分支预测以及多级缓存等方法来提升程序性能。多核技术在提升程序性能的同时,也带来了执行序列乱序和内存序列访问的乱序问题。与此同时,编译器也会基于自己的规则对代码进行优化,这些优化动作也会导致一些代码的顺序被重排。

首先,我们看一段代码,如下:

int A = 0;
int B = 0;

void fun() {
    A = B + 1; // L5
    B = 1; // L6
}

int main() {
    fun();
    return 0;
}

如果使用 g++ test.cc,则生成的汇编指令如下:

movl    B(%rip), %eax
addl    $1, %eax
movl    %eax, A(%rip)
movl    $1, B(%rip)

通过上述指令,可以看到,先把B放到eax,然后eax+1放到A,最后才执行B + 1。

而如果我们使用g++ -O2 test.cc,则生成的汇编指令如下:

movl    B(%rip), %eax
movl    $1, B(%rip)
addl    $1, %eax
movl    %eax, A(%rip)

可以看到,先把B放到eax,然后执行B = 1,再执行eax + 1,最后将eax赋值给A。从上述指令可以看出执行B赋值(语句L6)语句先于A赋值语句(语句L5)执行。

我们将上述这种不按照代码顺序执行的指令方式称之为指令乱序

对于指令乱序,这块需要注意的是:编译器只需要保证在单线程环境下,执行的结果最终一致就可以了,所以,指令乱序在单线程环境下完全是允许的。对于编译器来说,它只知道:在当前线程中,数据的读写以及数据之间的依赖关系。但是,编译器并不知道哪些数据是在线程间共享,而且是有可能会被修改的。而这些是需要开发人员去保证的。

那么,指令乱序是否允许开发人员控制,而不是任由编译器随意优化?

可以使用编译选项停止此类优化,或者使用预编译指令将不希望被重排的代码分隔开,比如在gcc下可用asm volatile,如下:

void fun() {
    A = B + 1;
    asm volatile("" ::: "memory");
    B = 0;
}

类似的,处理器也会提供指令给开发人员使用,以避免乱序控制,例如,x86,x86-64上的指令如下:

lfence (asm), void _mm_lfence(void)
sfence (asm), void _mm_sfence(void)
mfence (asm), void _mm_mfence(void)

为什么需要内存模型

多线程技术是为了最大限度的压榨cpu,提升计算能力。在单核时代,多线程的概念是在宏观上并行,微观上串行,多线程可以访问相同的CPU缓存和同一组寄存器。但是在多核时代,多个线程可能执行在不同的核上,每个CPU都有自己的缓存和寄存器,在一个CPU上执行的线程无法访问另一个CPU的缓存和寄存器。CPU会根据一定的规则对机器指令的内存交互进行重新排序,特别是允许每个处理器延迟存储并且从不同位置装载数据。与此同时,编译器也会基于自己的规则对代码进行优化,这些优化动作也会导致一些代码的顺序被重排。这种指令的重排,虽然不影响单线程的执行结果,但是会加剧多线程访问共享数据时的数据竞争(Data Race)问题。

以上节例子中的A、B两个变量为例,在编译器将其乱序后,虽然对于当前线程是没问题的。但是在多线程环境下,如果其它线程依赖了A 和 B,会加剧多线程访问共享数据的竞争问题,同时可能会得到意想不到的结果。

正是因为指令乱序以及多线程环境数据竞争的不确定性,我们在开发的时候,经常会使用信号量或者锁来实现同步需求,进而解决数据竞争导致的不确定性问题。但是,加锁或者信号量是相对接近操作系统的底层原语,每一次加锁或者解锁都有可能导致用户态和内核态的互相切换,这就导致了数据访问开销,如果锁使用不当,可能会造成严重的性能问题,所以就需要一种语言层面的机制,既没有锁那样的大开销,又可以满足数据访问一致性的需求。2004年,Java5.0开始引入适用于多线程环境的内存模型,而C++直到C++11才开始引入。

**Herb Sutter**在其文章中这样来评价C++11引入的内存模型:

The memory model means that C++ code now has a standardized library to call regardless of who made the compiler and on what platform it’s running. There’s a standard way to control how different threads talk to the processor’s memory.

“When you are talking about splitting [code] across different cores that’s in the standard, we are talking about the memory model. We are going to optimize it without breaking the following assumptions people are going to make in the code,” Sutter said

从内容可以看出,C++11引入Memory model的意义在于有了一个语言层面的、与运行平台和编译器无关的标准库,可以使得开发人员更为便捷高效的控制内存访问顺序。

一言以蔽之,引入内存模型的原因,有以下几个原因:

  • 编译器优化:在某些情况下,即使是简单的语句,也不能保证是原子操作
  • CPU out-of-order:CPU为了性能,可能会调整指令的执行顺序
  • CPU Cache不一致:在CPU Cache的影响下,在某个CPU下执行了指令,不会立即被其它CPU所看到

关系术语

为了便于更好的理解后面的内容,我们需要理解几种关系术语。

sequenced-before

sequenced-before是一种单线程上的关系,这是一个非对称,可传递的成对关系。

在了解sequenced-before之前,我们需要先看一个概念evaluation(求值)

对一个表达式进行求值(evaluation),包含以下两部分:

  • value computations: calculation of the value that is returned by the expression. This may involve determination of the identity of the object (glvalue evaluation, e.g. if the expression returns a reference to some object) or reading the value previously assigned to an object (prvalue evaluation, e.g. if the expression returns a number, or some other value)
  • Initiation of side effects: access (read or write) to an obj
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值