转自: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. Linux-3.14.12内存管理笔记【构建内存管理框架(1)】

    传统的计算机结构中,整个物理内存都是一条线上的,CPU访问整个内存空间所需要的时间都是相同的.这种内存结构被称之为UMA(Uniform Memory Architecture,一致存储结构).但是随 ...

  2. Python语法速查: 5. 运算符、math模块、表达式

    返回目录 (1)一些较容易搞错的运算符 一般简单的如加减乘除之类的运算符就不写了,这里主要列些一些容易搞错或忘记的运算符.运算符不仅仅只有号,有一些英文单词如 in, and 之类,也是运算符,并不是 ...

  3. 前端JSON请求转换Date问题

    目的:记录使用SpringMVC中前端JSON数据中的日期转换成Date数据类型时区产生的问题 记录下遇到过的问题 在使用SpringMVC框架中,使用@RequestBody注解将前端的json数据 ...

  4. Linux 和 Windows 查看 CUDA 和 cuDNN 版本

    目录 Linux 查看 CUDA 版本 查看 cuDNN 版本 Windows 查看 CUDA 版本 查看 cuDNN 版本 References Linux 查看 CUDA 版本 方法一: nvcc ...

  5. 66000][12505] Listener refused the connection with the following error: ORA-12505, TNS:listener does not currently know of SID given in connect descriptor oracle.n et.ns.NetException: Listener refuse

    新装的idea开发工具后连接数据库出现如题所示错误. 1.网上搜了不少的文章,没有解决我的问题.后来细心看了一下url: 一开始url是这样子的. jdbc:oracle:thin:@:s21_pdb ...

  6. Python程序中的进程操作-进程间通信(multiprocess.Queue)

    目录 一.进程间通信 二.队列 2.1 概念介绍--multiprocess.Queue 2.1.1 方法介绍 2.1.2 其他方法(了解) 三.代码实例--multiprocess.Queue 3. ...

  7. Python爬取6271家死亡公司数据,一眼看尽十年创业公司消亡史!

    ​ 小五利用python将其中的死亡公司数据爬取下来,借此来观察最近十年创业公司消亡史. 获取数据 F12,Network查看异步请求XHR,翻页. ​ 成功找到返回json格式数据的url, 很多人 ...

  8. 最热门的 10 个 Java 微服务框架

    1.Spring Boot Java 构建 Spring 应用程序已经有很长一段时间了,Spring Boot 是 Spring 的一个特定版本,它通过对配置细节的处理,使微服务构建更加简便.创建 S ...

  9. IT兄弟连 Java语法教程 流程控制语句 控制循环结构2

    使用continue忽略本次循环剩下的语句 continue的功能和break有点类似,区别是continue只是忽略本次循环剩下的语句,接着开始下一次循环,并不会终止循环:而break则是完全终止循 ...

  10. MySQL属性SQL_MODE学习笔记

    最近在学习<MySQL技术内幕:SQL编程>并做了笔记,本博客是一篇笔记类型博客,分享出来,方便自己以后复习,也可以帮助其他人 SQL_MODE:MySQL特有的一个属性,用途很广,可以通 ...