很多现代高级语言都提供了多线程并发技术。现在的服务器CPU基本上都是多核架构。在Java中,JVM可以适当地配置机器指令并重新排序以最大化机器性能。Java中有两种指令重排。第一次发生在将字节码编译成机器码的阶段,第二次发生在CPU执行的时候,指令也进行了适当的重新排列。写这篇文章的目的是弄清楚乱序的cpu指令。熟悉计算机底层系统的人都会知道,程序中每一行代码的执行顺序都可能被编译器和CPU按照一定的策略打乱。执行可以尽可能并行化。知道指令的乱序策略很重要,因为我们可以通过barrier(内存屏障)等指令,在正确的位置告诉cpu或者编译器,我这里可以接受乱序,但是我不能在那里接受乱序等。因此,可以在保证代码正确性的前提下,最大限度地发挥机器的性能。10多年前的程序员应该对处理器乱序执行和内存壁垒不陌生,但是随着计算机技术的飞速发展,我们离底层原理越来越远。这不是坏事,但在某些情况下了解一些基本原则会帮助我们更好地工作。例如,现代高级语言提供多线程并发技术。如果不深入,就很难排查和理解多线程带来的一些问题。前言我不打算在这里讨论编译器的乱序策略。这里讨论的指令乱序的含义更广,包括在多核上分别执行的指令之间在时间维度上的乱序。如果在多核CPU层面考虑乱序执行,我们需要梳理一下几个概念:单核和多核,乱序执行和顺序提交,storebuffer和invalidqueue。最后总结一下x86和arm/power架构的异同点。单核vs多核从多核的角度来看,有乱序的可能。例如,假设有一个变量x=0,在cpu0上写入W0(x,1),将1写入x。然后在cpu1上,读取R1(x,0)以获得x=0,这在x86和arm/powercpus上都是可能的。原因是x86上cpu核心、缓存和内存之间有storebuffer。当W0(x,1)执行成功后,修改只存在于storebuffer中,并没有写入到cache和内存中,所以cpu1读取的小于最新的x-value。对于arm/power,也有storebuffer,可能存在invalidqueue,导致cpu1无法读取最新的x值。对于没有invalidqueue的x86系列CPU,当修改从storebuffer刷新到缓存时,可以保证其他核上可以读取到最新的修改。但是,对于具有无效队列的CPU而言,情况不一定如此。为了保证多核之间修改的可见性,我们在写程序的时候需要加入内存屏障,比如x86上的mfence指令。乱序执行vs顺序提交我们知道,为了让指令在cpu中尽可能的并行执行,就发明了流水线技术。但是,如果两条指令之间存在依赖关系,如数据依赖、控制依赖等,则后一条语句必须等到前一条指令完成后才能开始。为了提高流水线的运行效率,cpu会做出,例如:1)对非依赖和前置指令进行适当的乱序和调度;2)控制相关指令的分支预测;3)读内存等操作耗时,做预读;等等。以上种种都会导致秩序混乱的可能。但是对于x86CPU来说,从单核的角度来看,它实际上提供了Sequentialconsistency[1]的一致性保证。wiki上对顺序一致性的定义如下:“......任何执行的结果都是相同的,就好像所有处理器的操作都按某种顺序执行,并且每个处理器的操作都按照这个顺序出现在它的程序指定的顺序。”也就是说,要满足Sequentialconsistency,就要保证每个处理器的指令执行顺序必须和程序给定的顺序一致。奇怪不是吗?这不和我刚才说的指令乱序优化矛盾吗?其实并不矛盾。指令在CPU内核内部确实是乱序执行和调度的,但为了外部性能,它们是按顺序提交的。如果将ISA寄存器(如EAX、EBX等)和storebuffer作为cpu的对外接口,cpu只需要将内部真实的物理寄存器按照执行顺序依次映射到ISA寄存器即可指令,即cpu只需要将结果按顺序提交给ISA寄存器,就可以保证Sequentialconsistency。当然,以上是针对x86架构的CPU。ARM/Power架构CPU在单核上的一致性保证较弱。不需要保证Sequentialconsistency,所以不需要顺序提交。你只需要保证控制依赖、数据依赖、地址依赖等指令的顺序就足够了。为了保证这些弱一致性模型CPU下不相关指令之间的提交顺序,需要使用barrier指令。StoreBuffer&InvalidQueue存储缓冲区存在于CPU内核和缓存之间。对于x86架构,storebuffer是FIFO,所以不会出现乱序,写入顺序就是flush到cache中的顺序。但是对于ARM/Power架构来说,storebuffer不保证FIFO,所以先写入storebuffer的数据可能会比后写入storebuffer的数据刷入缓存更晚。从这个角度来看,storebuffer的存在会让ARM/Power架构出现乱码。storebarrier的意思就是将storebuffer中的数据flush到cache中。在某些cpu中,存在无效队列。invalidqueue用于缓存cacheline的失效消息,即cpu0写入W0(x,1)并将修改从storebuffer刷入缓存时,此时cpu1仍允许读取R1(x,0)的。因为使缓存行失效的消息缓存在无效队列中,所以还没有应用到缓存行中。这也有可能使指令乱序。loadbarrier存在的意义就是刷新无效的队列缓冲区。X86vsARM/Power对于基于x86的CPU,在单核基础上,它保证了顺序一致性。因此,对于开发者来说,我们不必担心单核系统上的乱序优化会给我们的程序带来正确性。性问题。从多核的角度保证了x86-tso模型,storebuffer中的数据可以使用mfence写入cache。而且,由于在x86架构下,storebuffer是FIFO,没有无效队列,mfence可以保证数据在多核间的可见性和顺序性。[2]对于具有arm和power架构的CPU,编程变得更加危险。除了指令前后的数据依赖、控制依赖、地址依赖不能乱序外,其他指令之间也可能乱序。而且它们的storebuffer不是FIFO,可能会出现无效队列,这也给并发编程带来了困难。因此,需要引入不同类型的屏障来满足不同的要求。[3]小结从上面的介绍中,我们可以知道开发者想要做好并发编程是多么的困难,但是我们至少迈出了第一步,就是定义困难本身。
