指令重排
一. 指令重排的分类:
- 编译器优化的重排
- 指令级并行的重排
- 内存系统的重排
编译器优化重排序和指令级并行重排序不会影响程序的结果,因此程序员需要着重处理的是内存系统重排序带来的问题。
二. 指令重排的原理:
1.编译器优化的重排:
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
2.指令级并行的重排:
为了提高CPU的执行效率,现代的CPU基本上都支持指令流水线。
2.1 指令流水线:
一条指令要执行要经过3个阶段:取指令、译码、执行,每个阶段都要花费一个机器周期,如果没有采用流水线技术,那么这条指令执行需要3个机器周期;如果采用了指令流水线技术,那么当这条指令完成“取指”后进入“译码”的同时,下一条指令就可以进行“取指”了,这样就提高了指令的执行效率。
指令步骤的并行。常见的六级流水线将指令流的处理过程划分为取指(FI)、译码(DI)、计算操作数地址(CO)、取操作数(FO)、执行指令(EI)、写操作数(WO)等几个并行处理的过程段。这就是指令6级流水时序。在这个流水线中,处理器有六个操作部件,同时对这六条指令进行加工,加快了程序的执行速度。几乎所有的高性能计算机都采用了指令流水线。
2.2 流水线停顿:
处理器的流水线设计中有流水线中的冲突(Hazards)问题:主要分为资源冲突和数据冲突。
- 资源冲突:
资源冲突是指流水线中硬件资源的冲突,最常见的是运算单元的冲突,譬如除法器需要多个时钟周期才能完成运算,因此在前一条除法指令运算完成之前,新的除法指令如果也需要除法器则会存在着资源冲突。在处理器的流水线中硬件资源冲突种类还有较多,在此不做一一赘述。解决资源冲突的方法可以通过复制硬件资源或者流水线停顿等待硬件资源的方法解决。 - 数据冲突:
数据冲突是指不同的指令之间的操作数存在数据相关性造成的冲突。常见的数据相关性包括:- WAR(Write-After-Read)相关性,又称先读后写相关性;
- WAW(Write-After-Write)相关性,又称先写后写相关性;
- RAW(Read-After-Write)相关性,又称先写后读相关性。
2.3 停顿的优化:
- 采用数据旁路传播(Data Bypass and Forward)技术尽可能的让前序指令的计算结果更快的旁路传播给后序相关指令的操作数;
- 尽可能的让后序相关指令在等待的过程中不阻塞流水线而让其他无关的指令能够继续顺利执行。
3. 内存系统的重排:
3.1 Memory Bank 的划分 :
一般 Memory bank 是按 cache address 来划分的。比如 偶数 adress 0×12345000 分到 bank 0, 奇数 address 0×12345100 分到 bank1
3.2 重排序分析:
理想的内存访问指令顺序:
- CPU0 往 cache address 0×12345000 写入一个数字 1 。因为address 0×12345000 是偶数,所以值被写入 bank0.
- CPU1 读取 bank0 address 0×12345000 的值,即数字1 。
- CPU0 往 cache 地址 0×12345100 写入一个数字 2 。因为address 0×12345100 是奇数,所以值被写入 bank1.
- CPU1 读取 bank1 address 0×12345100 的值,即数字2 。
重排序后的内存访问指令顺序:
- CPU0 准备往 bank0 address 0×12345000 写入数字 1 。
- CPU0 检查 bank0 的可用性。发现 bank0 处于 busy 状态。
- CPU0 为了防止 cache 等待,发挥最大效能,将内存访问指令重排序。即先执行后面的 bank1 address 0×12345100 数字2 的写入请求。
- CPU0 检查 bank1 可用性,发现bank1 处于 idle 状态。
- CPU0 将数字2 写入 bank 1 address 0×12345100 。
- CPU1 来读取 0×12345000 ,未读到 数字1 ,出错。
- CPU0 继续检查 bank0 的可用性,发现这次 bank0 可用了,然后将数字1 写入 0×12345000 。
- CPU1 读取 0×12345100 ,读到数字2 ,正确。
从上述触发步骤中,可以看到第 3 步发生了指令重排序,并导致第 6 步读到错误的数据。
通过对指令重排,CPU 可以获得更快地响应速度, 但也给编写并发程序的程序员带来了诸多挑战。
3.3 解决重排序带来的问题:
内存屏障是用来防止CPU 出现指令重排序的利器之一。
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处 理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂 贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。