拾遗增补-指令重排序

6.7. 指令重排序

6.7.1. 指令重排序的介绍

1)指令重排序的类型
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
编译器优化的重排序 编译器在不改变单线程程序语义的前提下(代码中不包含synchronized关键字),可以重新安排语句的执行顺序。
指令级并行的重排序 现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
重排序又可以分为两大类,一是编译器重排序:编译器优化重排序,二是处理器重排序:指令级并行重排序、内存系统重排序。
2)重排序的流程
从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

在这里插入图片描述

3)存在的问题
这三种重排序方法都可能会导致多线程程序出现内存可见性问题。

6.7.2. 内存系统重排序问题

1)写缓冲区的作用
现代的处理器使用写缓冲区来临时保存向内存写入的数据。
2)写缓冲区造成的优势
优势1,合并写缓冲区中对同一内存地址的多次写操作,可以减少对内存总线的占用;优势2,通过以批处理的方式刷新写缓冲区;优势3,写缓冲区可以保证指令流水线持续运行,避免由于处理器停顿下来等待向内存写入数据而产生的延迟。
3)写缓冲区的问题
每个处理器上的写缓冲区,仅仅对它所在的处理器可见,对其他处理器并不可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!
4)允许指令重排序的列表
下面是常见处理器允许的重排序类型的列表:
在这里插入图片描述

注:上表单元格中的“N”表示处理器不允许两个操作重排序,“Y”表示允许重排序。
从上表我们可以看出:常见的处理器都允许Store-Load重排序;常见的处理器都不允许对存在数据依赖的操作做重排序。sparc-TSO和x86拥有相对较强的处理器内存模型,它们仅允许对写-读操作做重排序(因为它们都使用了写缓冲区)。

6.7.3. 内存屏障指令

1)使用背景
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
2)基本使用
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:
在这里插入图片描述

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)

6.7.4. happens-before关系

1)基本介绍
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
2)happens-before规则
与程序员密切相关的happens-before规则如下:
程序顺序规则 一个线程中的每个操作,happens- before 于该线程中的任意后续操作
监视器锁规则 对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁
volatile变量规则 对一个volatile域的写,happens- before 于任意后续对这个volatile域的读
传递性 如果A happens- before B,且B happens- before C,那么A happens- before C
Start规则
Join规则
特殊说明:两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
3)happens-before与JMM的关系
在这里插入图片描述

如上图所示,一个happens-before规则通常对应于多个编译器和处理器重排序规则。对于java程序员来说,happens-before规则简单易懂,避免java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现。

6.7.5. as-if-serial语义

1)数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:
在这里插入图片描述

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。
注:这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
2)as-if-serial语义
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变,编译器,runtime 和处理器都必须遵守as-if-serial语义。即编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值