走进C++11(三十九) 内存模型(二)

关注公众号获取更多信息:

 

今天继续聊内存模型。

 

上篇文章讲了一些概念。今天讲一些小细节,为最后的总结做铺垫。

 

 

 

图片

 

上图所示的是一个典型的多核CPU系统架构,它包含有2个CPU核,每个CPU核有一个私有的32KB 的 L1 cache,两个CPU 核共享 1MB的 L2 cache 以及 512MB的主存。

 

在这个内存模型下,cpu写数据并不是立即写入RAM中,而是写入L1 cache,再从L1 cache存入(store) RAM中,读数据也是先从L1 cache中读,读不到再从RAM中读,这种读写数据的模式是能够提高数据存取效率的,但是在一些特殊情况下会导致程序出错,考虑以下这个例子。

 

                    x=y=0;
            Thread 1      Thread 2
              x = 1;        y = 1;
             r1 = y;       r2 = x;

 

 

表面上看,r1==r2==0这种输出是不可能出现的,然而,有一种可能性是,由于r1不依赖于x,编译器可以把r1=y这步操作调整到x=1这步操作之前,同样,r2=x这步操作可以调整到y=1这步操作之前,这样一来,core 1可以先读取L1 cache中的y的值,core 2 才执行 y = 1的赋值操作,同理,r2 = x这步操作也可以在x=1这步赋值操作之前执行,这时候就会出现r1 == r2 ==0的输出结果。

 

为什么会出现这种情况呢?

 

从软件层面来说,这个锅是编译器的

 

编译器了解底层CPU的思维模式,因此,它可以在将c翻译成汇编的时候进行优化(例如内存访问指令的重新排序),让产出的汇编指令在CPU上运行的时候更快。然而,这种优化产出的结果未必符合程序员原始的逻辑。

 

从CPU层面来说

 

在运行时,CPU 会乱序执行指令,乱序处理器(Out-of-order processors)处理指令通常有以下几步:

 

  1. 指令获取

  2. 指令被分发到指令队列

  3. 指令在指令队列中等待,直到输入操作对象可用(一旦输入操作对象可用,指令就可以离开队列,即便更早的指令未被执行)

  4. 指令被分配到适当的功能单元并执行

  5. 执行结果被放入队列(而不立即写入寄存器堆)

  6. 只有所有更早请求执行的指令的执行结果被写入寄存器堆后,指令执行的结果才被写入寄存器堆(执行结果重排序,让执行看起来是有序的)

 

从上面的执行过程可以看出,乱序执行相比有序执行能够避免等待不可用的操作对象(有序执行的第二步)从而提高了效率。现代的机器上,处理器运行的速度比内存快很多,有序处理器花在等待可用数据的时间里已经可以处理大量指令了。


 

我们先不考虑具体的软件或具体的硬件情况,仅仅从逻辑上考虑这个问题。

 

最简单的模型就是只简化内存操作为Store(内存存储)和Load(内存读取)这两个操作,因此2×2可以组合出至少以下4种序列。

 

  • Store, Load (SL)

  • Store, Store (SS)

  • Load, Load (LL)

  • Load, Store (LS)

 

 

如果某个barrier可以保证Store,Load不被乱序执行,则我们称其为SL memory barrier(SLB)。类似的我们可以定义出SSB, LLB, LSB。

 

 

举个例子,为了保证执行顺序,我们可以插入一个SSB。

 

 

int port, action;void forbar(int base){action = 4; __asm__("SSB"); port = base + 3;}

 

这样我们就可以保证两个store是按顺序执行的。

 

c++11定义的模型比这个要更复杂,大概有以下

 

  • full memory barrier(mb)

  • release memory barrier(relb)

  • acquire memory barrier(acqb)

  • write memory barrier(wb)

  • read memory barrier(rb)

  • data dependency memory barrier(ddrb)

 

为啥不是2*2=4种情况呢?

 

这就涉及到Acquire与Release语义

 

Acquire与Release是无锁编程中最容易混淆的两个原语,它们是线程之间合作进行数据操作的关键步骤。在这里,借助前面对memory barrier的解释,对acquire与release的语义进行阐述。

 

  • acquire本质上是read-acquire,它只能应用在从RAM中read数据这种操作上,它确保了所有在acquire之后的语句不会被调整到它之前执行,如下图所示。

     

图片

用上面的memory barrier来描述,acquire等价于LoadLoad加上LoadStore栅栏。

 

  • release本质上是write-release,它只能应用在write数据到RAM中,它确保了所有在release之前的语句不会被调整到它之后执行,如下图所示。

图片

 

用上面的memory barrier来描述,release等价于LoadStore加上StoreStore栅栏。


发现没有,现在情况增加了一个维度,就是上边四种之前和之后。

 

比如relb这个的语义是release write memory barrier,要保护的序列是“anymemops, relb, action”这种序列,这个anymemops可以是多个store和load的任意组合,可以保证在执行关键操作前一定可以得到最新的内存信息。

 

而acqb就是acquire load memory barrier,要保护的序列是“action, acqb, anymemops”,可以保证执行关键操作后的anymemops是基于最新的内存数据。

 

顺便提一句,这个action往往就是各种atomic operation,但并非一定是。这也是memory barrier容易与原子操作混淆的一个原因。

 

 

好了就到这里,下次讲C++11的六种内存模型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值