摘要:处理器通过缓存能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。从上述触发步骤中,可以看到第步发生了指令重排序,并导致第步读到错误的数据。内存屏障是用来防止出现指令重排序的利器之一。
这两天,我拜读了 Dennis Byrne 写的一片博文Memory Barriers and JVM Concurrency (中译文内存屏障与JVM并发)。
文中提到:
对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。
这段话是作者对内存屏障重要性的定义。通过cache降低内存延迟,这句话很好理解。但后面那句“为了性能重排序内存操作顺序”,让没学好微机原理的我倍感疑惑。
CPU为何要重排序内存访问指令?在哪种场景下会触发重排序?作者在文中并未提及。
为了解答疑问,我在网上查阅了一些资料,在这里跟大家分享一下。
重排序的背景
我们知道现代CPU的主频越来越高,与cache的交互次数也越来越多。当CPU的计算速度远远超过访问cache时,会产生cache wait,过多的cache wait就会造成性能瓶颈。
针对这种情况,多数架构(包括X86)采用了一种将cache分片的解决方案,即将一块cache划分成互不关联地多个 slots (逻辑存储单元,又名 Memory Bank 或 Cache Bank),CPU可以自行选择在多个 idle bank 中进行存取。这种 SMP 的设计,显著提高了CPU的并行处理能力,也回避了cache访问瓶颈。
Memory Bank的划分
一般 Memory bank 是按cache address来划分的。比如 偶数adress 0×12345000分到 bank 0, 奇数address 0×12345100分到 bank1。
重排序的种类
编译期重排。编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以之更适合于CPU的并行执行。
运行期重排,CPU在执行过程中,动态分析依赖部件的效能,对指令做重排序优化。
实例讲解指令重排序原理为了方便理解,我们先来看一张CPU内部结构图。
从图中可以看到,这是一台配备双CPU的计算机,cache 按地址被分成了两块 cache banks,分别是cache bank0 和 cache bank1。
理想的内存访问指令顺序:
1,CPU0往cache address 0×12345000 写入一个数字 1。因为address 0×12345000是偶数,所以值被写入 bank0.
2,CPU1读取 bank0 address 0×12345000 的值,即数字1。
3,CPU0往 cache 地址 0×12345100 写入一个数字 2。因为address 0×12345100是奇数,所以值被写入 bank1.
4,CPU1读取 bank1 address 0×12345100 的值,即数字2。
重排序后的内存访问指令顺序:
1,CPU0 准备往 bank0 address 0×12345000 写入数字 1。
2,CPU0检查 bank0 的可用性。发现 bank0 处于 busy 状态。
3, CPU0 为了防止 cache等待,发挥最大效能,将内存访问指令重排序。即先执行后面的 bank1 address 0×12345100 数字2的写入请求。
4,CPU0检查 bank1 可用性,发现bank1处于 idle 状态。
5,CPU0 将数字2写入 bank 1 address 0×12345100。
6,CPU1来读取 0×12345000,未读到 数字1,出错。
7, CPU0 继续检查 bank0 的可用性,发现这次bank0 可用了,然后将数字1写入 0×12345000。
8, CPU1 读取 0×12345100,读到数字2,正确。
从上述触发步骤中,可以看到第 3 步发生了指令重排序,并导致第 6步读到错误的数据。
通过对指令重排,CPU可以获得更快地响应速度,但也给编写并发程序的程序员带来了诸多挑战。
内存屏障是用来防止CPU出现指令重排序的利器之一。
通过这个实例,不知道你对指令重排理解了没有?
X86仅在 Stores after loads 和 Incoherent instruction cache pipeline 中会触发重排。
Stores after loads的含义是在对同一个地址进行读写操作时,写入在读取后面,允许重排序。即满足弱一致性(Weak Consistency),这是最可被接受的类型,不会造成太大的影响。
Incoherent instruction cache pipeline是跟JIT相关的类型,作用是在执行self-modifying code 时预防JIT没有flush指令缓存。我不知道该类型跟指令排序有什么关系,既然不在本文涉及范围内,就不做深入探讨了。
参考资料
http://kenwublog.com/docs/memory.barrier.ppt
http://kenwublog.com/docs/memory.model.instruction.reordering.and.store.atomicity.pdf
http://kenwublog.com/docs/memory.ordering.in.modern.microprocessor.pdf
http://en.wikipedia.org/wiki/Memory_ordering
http://en.wikipedia.org/wiki/Memory_Bank
via ifeve
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/69760.html
摘要:并发编程的挑战并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度的并发执行。的实现原理与应用在多线程并发编程中一直是元老级角色,很多人都会称呼它为重量级锁。 并发编程的挑战 并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度的并发执行。如果希望通过多线程执行任务让程序运行的更快,会面临非常多的挑战:(1)上下文切换(2)死...
摘要:文章简介分析的作用以及底层实现原理,这也是大公司喜欢问的问题内容导航的作用什么是可见性源码分析的作用在多线程中,和都起到非常重要的作用,是通过加锁来实现线程的安全性。而的主要作用是在多处理器开发中保证共享变量对于多线程的可见性。 文章简介 分析volatile的作用以及底层实现原理,这也是大公司喜欢问的问题 内容导航 volatile的作用 什么是可见性 volatile源码分析 ...
摘要:本文会先阐述在并发编程中解决的问题多线程可见性,然后再详细讲解原则本身。所以与内存之间的高速缓存就是导致线程可见性问题的一个原因。原则上面讨论了中多线程共享变量的可见性问题及产生这种问题的原因。 Happens-Before是一个非常抽象的概念,然而它又是学习Java并发编程不可跨域的部分。本文会先阐述Happens-Before在并发编程中解决的问题——多线程可见性,然后再详细讲解H...
摘要:假设不发生编译器重排和指令重排,线程修改了的值,但是修改以后,的值可能还没有写回到主存中,那么线程得到就是很自然的事了。同理,线程对于的赋值操作也可能没有及时刷新到主存中。线程的最后操作与线程发现线程已经结束同步。 很久没更新文章了,对隔三差五过来刷更新的读者说声抱歉。 关于 Java 并发也算是写了好几篇文章了,本文将介绍一些比较基础的内容,注意,阅读本文需要一定的并发基础。 本文的...
阅读 2457·2021-09-27 13:36
阅读 2162·2019-08-29 18:47
阅读 2129·2019-08-29 15:21
阅读 1393·2019-08-29 11:14
阅读 1978·2019-08-28 18:29
阅读 1622·2019-08-28 18:04
阅读 568·2019-08-26 13:58
阅读 3206·2019-08-26 12:12