转自:https://blog.csdn.net/lizhihaoweiwei/article/details/50562732

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lizhihaoweiwei/article/details/50562732
“CPU执行乱序”是一个常见的话题,鉴于自己一直对这个概念存在些许理解的差池,故今日写一篇文章留作备忘。注,这里仅仅讨论CPU执行乱序,不涉及编译器的乱序。

1.CPU 为什么会乱序?
本质原因是CPU为了效率,将长费时的操作“异步”执行,排在后面的指令不等前面的指令执行完毕就开始执行后面的指令。而且允许排在前面的长费时指令后于排在后面的指令执行完。

如在 CPU0 上执行下面两句话:

a = 1;

b = 2;

在以下情况下 b=2 会先于 a=1 执行完:a 没有缓存于 CPU0 的 cache 上,而 b 缓存于 CPU0 的 cache上,且处于 Exclusive 状态。

在一个 CPU 上写入没有缓存的变量流程如下(该图来源于《cache 一致性》):

CPU0 不能仅仅在它的 cache 里写入 a=1,它还要告诉缓存 a 所在的 CPU:你上面的 a 缓存过期(invalidate)了! 等 CPU0 收到响应(Invalidate Ack)后,才能写入。

这一通信过程是需要耗费时间,而且距离收到 ack 的时间是不确定的,这限制于总线的繁忙程度以及 CPU1 是否在执行优先级高的任务等等。所以CPU0 不能干等着,它要向后继续执行指令:b=2,而 b 位于本 cache 上且处于 Exclusive 状态,可以直接修改b的值(b 变为 Modified 状态)。此时b=2已经执行完毕,而 a=1还没有执行完毕!从时序上来讲,这就是乱序执行。

[知识展开]
MESI 是一种常用的用于保证 CPU 的缓存一致性的协议,规定了位于缓存中的变量在不同时序下处的状态以及经过某些操作相互转化的状态。严格遵守这个协议可以保证缓存一致。这篇文章很好的讲解了MESI

2.CPU 乱序的解决思路?
CPU 执行乱序主要有以下几种:

一.写写乱序(store store):a=1;b=2-------------> b=2;a=1;

二.写读乱序(store load): a=1;load(b); ------------> load(b);a=1;

三.读读乱序(load load):  load(a);load(b);  -----------> load(b);load(a);

四.读写乱序(load store): load(a);b=2; ------------> b=2;load(a);

C++ 提供的内存模型约束,另一篇文章中提到了,可以灵活地根据需要对内存加以约束,达到强度不同的顺序执行效果,如全局的顺序,局部代码的顺序,逻辑相关的顺序等。这不是本文的重点,本文只笼统的使用读栅栏 lfence 和写栅栏 sfence 这两种约束。先给出现代的 CPU 架构:(该图来源于《cache 一致性》):

lfence : load fence,即,使得 CPU 应用 invalidate queue,使某个缓存失败,去其它 CPU 同步数据。

sfence: store fence,即,使得 CPU 应用 store buffer ,同步到 cache 中。

下面举例说明这两个内存栅栏的应用。

在不使用内存栅栏的情况下:

CPU0 执行

a = 1;
b = 1;

CPU1 执行

while (b != 1);
assert (a == 1);

CPU1的 assert 有可能会失败:只要 a 不位于 CPU0 的缓存上,b 不位于 CPU1 的缓存上。

CPU0 需要发出 read invalidate 的消息去改写 a 的值,此消息被 CPU1 接受到后立即响应:invalidate ack 并在 invalidate queue 中加入这条消息,但 CPU1 并没有实时处理此消息,而是等到合适的时机再去处理。

CPU1 需要发出 read 的消息去获得 b 的值,此消息被 CPU0 接受到后立即响应 b 的值。

一种导致assert失败的执行顺序如下:

1. CPU0 和 CPU1 同时开始执行 a=1 和 while(b!=1).

CPU0 将 a=1写入 store buffer,发送 read invalidate 消息

CPU1 发送 (remote)read 消息

2.CPU0 执行完 b=1后收到 read 消息,CPU1 收到 read invalidate 消息.

CPU0 直接写入 CPU1 的 cache line: b=1

CPU1 在 invalidate queue 中标记 a,表示CPU1中的 a 值无效,返回 invalidate ack

3.CPU0执行完毕;CPU1 取它缓存中的 a=0 执行 assert(a==1),失败

思考上面失败的原因,如果能保证两点即避免:

1.CPU1 在最后 assert(a==1) 时,如果能先去 invalidate queue 中查看到 a 是无效的,再去 CPU0 中请求 a 的最新值,则这一步不会失败。

2.还有一点需要保证:b==1 时必然已经 a==1。这就要求CPU0上的 a==1 的执行效果先于 b==1 上完成。也就是,先写入 a==1,再写入 b==1。

分别针对以上两点:load memory fence 可以保证1,store memory fence 可以保证2.

load memory fence (lfence)做的事情:处理 invalidate queue ,如果有已经失效的值,则重新请求。

store memory fence (sfence)作的事情:处理 store buffer,标记 store buffer 里的值(store buffer 有一个设计特点:如果 store buffer 里面还存在标记的量,则新写入的量不会立即写入到 cache 中[哪怕是此量在 cache 中],而是写入到 store buffer 中,该量不作标记。等到合适的时机,store buffer 被刷入cache 中时,保证先将被标记的量写入 cache,末被标记的量后写入。这就达成了写入有序性)。

于是上面的问题可以这样解决:

CPU0 执行

a = 1;

sfence(); //保证先写入 a,再写入 b
b = 1;

CPU1 执行

while (b != 1);

lfence(); //保证取得最新的 a值。
assert (a == 1);

这里需要提一点,由于 sence 和 lfence 的实现原理(即它们分别做的事情)
sfence 具有 release 语义:位于 release 后面的代码不可能比它前面的代码先开始执行。之所以会有该语义,是因为 sfence 操作被强制成同步的操作。必须完成才能继续后面的指令。

lfence 具有 acquire 语义:位于 acquire 前面的代码不可能比它后面的代码后开始执行。之所以会有该语义的原因同上。

3.CPU 存储模型简单解析
简单地分析一下,为什么 CPU 中会有 store buffer 和 invalidate queue。

store buffer
在没有 store buffer 时,CPU 写入一个量,有以下情况。

1.量不在该 CPU 缓存中,则需要发送 read invalidate 信号,再等待此信号返回,之后再写入量到缓存中。

2.量在该 CPU 缓存中,如果该量的状态是 exclusive 则直接更改。而如果是 shared 则需要发送 invalidate 消息让其它 CPU 感知到这一更改后再更改。

这些情况中,很有可能会触发该 CPU 与其它 CPU 进行通讯,接着需要等待它们回复。这会浪费大量的时钟周期!为了提高效率,可以使用异步的方式去处理:先将值写入到一个 buffer 中,再发送通讯的信号,等到信号被响应,再应用到 cache 中。并且,此 buffer 能够接受该 CPU 读值。这个 buffer 就是 store buffer。而不等对某个量的赋值指令的完成,继续下一条指令,去 store buffer 中读该量的值,这种优化叫 store forwarding.

invalidate queue
同理,解决了主动发送信号端的效率问题,那么,接受端 CPU 接受到 invalidate 信号后如果立即采取相应行动(去其它 CPU 同步值),再返回响应信号,则时钟周期也太长了,此处也可优化。接受端 CPU 接受到信号后不是立即采取行动,而是将 invalidate 信号插入到一个 queue中,立即作出响应。等到合适的时机,再去处理这个 queue 中的 invalidate,作相应处理。这个 queue 就是 invalidate queue。

参考文献
深入Java内存模型 
http://www.importnew.com/10589.html

深入理解Java内存模型(四)
http://www.infoq.com/cn/articles/java-memory-model-4/

何登成
http://hedengcheng.com/

几种不同的 fence
http://www.cnblogs.com/catch/p/3803130.html

学习load acquire 和store release
http://blog.csdn.net/summerhust/article/details/7406479

leveldb AotmicPointer 实现
http://brg-liuwei.github.io/tech/2014/10/15/leveldb-1.html

leveldb AotmicPointer 实现 2
http://huchh.com/2015/12/03/leveldb-atomicpointer/

store buffer & invalidate
http://www.importnew.com/10589.html

C++ 内存模型
http://blog.csdn.net/pongba/article/details/1659952

CPU cache 文集
http://blog.csdn.net/henzox/article/details/40427463

指令重排举例

http://www.zgxue.com/164/1645056.html
————————————————
版权声明:本文为CSDN博主「lizhihaoweiwei」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lizhihaoweiwei/article/details/50562732

cpu 乱序执行与问题【转】的更多相关文章

  1. CPU乱序执行问题

    CPU为了提高执行效率,会在一条指令执行的过程中(比如去内存读数据,读数据的过程相较于CPU的执行速度慢100倍以上,cpu处于等待状态),这个时候cpu会分析接下来的指令是否正在执行的指令相关联,如 ...

  2. cpu乱序执行

    http://blog.163.com/zhaojie_ding/blog/static/1729728952007925111324379/?suggestedreading 处理器的乱序和并发执行 ...

  3. CPU乱序执行基础 —— Tomasulo算法及执行过程

    朋友们可以关注下我的公众号,获得最及时的更新: IBM 360/91浮点单元最先实现Tomasulo算法从而允许乱序执行.360体系只有4个双精度浮点寄存器,限制了编译器调度的有效性.而且,IBM 3 ...

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

    目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...

  5. CPU Meltdown和Spectre漏洞分析

    一.背景: 1月4日,国外爆出了整个一代处理器都存在的灾难性漏洞:Meltdown和Spectre. 几乎影响了全球20年内所有cpu处理器:这两个漏洞可以使攻击者通过利用并行运行进程的方式来破坏处理 ...

  6. 现代cpu的合并写技术对程序的影响

    对于现代cpu而言,性能瓶颈则是对于内存的访问.cpu的速度往往都比主存的高至少两个数量级.因此cpu都引入了L1_cache与L2_cache,更加高端的cpu还加入了L3_cache.很显然,这个 ...

  7. 浅谈原子操作、volatile、CPU执行顺序

    浅谈原子操作.volatile.CPU执行顺序 在计算机发展的鸿蒙年代,程序都是顺序执行,编译器也只是简单地翻译指令,随着硬件和软件的飞速增长,原来的工具和硬件渐渐地力不从心,也逐渐涌现出各路大神在原 ...

  8. C++11 并发指南七(C++11 内存模型一:介绍)

    第六章主要介绍了 C++11 中的原子类型及其相关的API,原子类型的大多数 API 都需要程序员提供一个 std::memory_order(可译为内存序,访存顺序) 的枚举类型值作为参数,比如:a ...

  9. 内存屏障(Memory barrier)-- 转发

    本文例子均在 Linux(g++)下验证通过,CPU 为 X86-64 处理器架构.所有罗列的 Linux 内核代码也均在(或只在)X86-64 下有效. 本文首先通过范例(以及内核代码)来解释 Me ...

随机推荐

  1. Oracle 快速配置连接服务

    [net Manager配置] oracle客户端连接,或者代码连接时,需要配置本地连接服务. [快速配置] ①找到 tnsnames.ora 文件,使用记事本打开 ② TEST{#服务名} = (D ...

  2. WordPress疑难问题以及解决方案汇总

    版权声明:本文为xing_star原创文章,转载请注明出处! 本文同步自http://javaexception.com/archives/101 WordPress疑难问题以及解决方案汇总: 最近这 ...

  3. 【三】Gradle中的Task

    gradle中,最经常被使用的,一个task,一个是dependencies 1.Task声明 task默认是DefaultTask类, Task中有两个属性 group description,最佳 ...

  4. ORACLE DATAGUARD 日志传输状态监控

    ORACLE DATAGUARD的主备库同步,主要是依靠日志传输到备库,备库应用日志或归档来实现.当主.备库间日志传输出现GAP,备库将不再与主库同步.因此需对日志传输状态进行监控,确保主.备库间日志 ...

  5. mssql sqlserver 将逗号分隔的一列数据转换为多列数据的方法分享

    转自:http://www.maomao365.com/?p=10278  摘要: 下文讲述sqlserver中将使用逗号组合的单列数据,分隔为多列数据的方法 实验环境:sql server 2012 ...

  6. PyCharm将选中的内容加上引号

    正常情况下,选中一段内容,再按引号,内容会被替换为引号 想要将选中的内容不被替换为引号,而在内容的两端加上引号,只需设置一下即可 File  -->  Settings  -->  Edi ...

  7. 【poj2661】Factstone Benchmark(斯特林公式)

    传送门 题意: 给出\(x,x\leq 12\),求最大的\(n\),满足\(n!\leq 2^{2^x}\). 思路: 通过斯特林公式: \[ n!\approx \sqrt{2\pi n}\cdo ...

  8. GNS3 2.18 + ASA(IOU)

    使用软件及版本 地址:https://www.gns3.com/ gns3: 2.1.18 ASA:asa842-initrd asa842-vmlinuz 一.gns3 vm安装 1.安装 注意:启 ...

  9. Linux下安装redis报错信息

    redis在Linux安装报错 标签: redislinuxcentos 2017-02-24 13:46 384人阅读 评论(0) 收藏 举报  分类: Linux安装工具(2)  版权声明:本文为 ...

  10. 算法问题实战策略 WORDCHAIN

    地址  https://algospot.com/judge/problem/read/WORDCHAIN 解答: 1 书上的解法是制作有向图 然后查找欧拉回路  代码实现稍后 假设一定存在欧拉路径的 ...