美文网首页计算机系统
浅谈运行期间cpu指令重排

浅谈运行期间cpu指令重排

作者: Teech | 来源:发表于2019-07-29 14:48 被阅读0次

上篇谈到了编译器会进行内存操作指令的重排,这篇来谈谈运行期间cpu进行内存操作指令重排。当且仅当lock-free技术采用时才会出现这个情况,换句话说在多线程之间没有任何互斥操作在操作同一块共享内存期间。不像编译期间内存操作指令重排一样,这种情况只有在多核的系统中才会发生。
我们可以使用memory barrier指令来保证内存操作次顺的一致性。下面列举了一些可以被当做屏障的指令:

  • 显式的gcc的汇编指令 比如 asm volatile("lwsync" ::: "memory")
  • 很多c++11的原子操作,比如load(std::memory_order_acquire)
  • 互斥锁 比如pthread_mutex
    确实有很多指令可以被充当成memory barrier,也有很多不同类型的memory barrier。确实,不同类型的操作产生不同类型的屏障指令。下面详细的谈谈这方面的内容。
    我们假设一个多核系统的架构,有L1,L2两级缓存(实际上现在普遍都是3级),每个核心分布都有私有的32KB的L1缓存。1MB的L2缓存被两个核共享了,还有512MB的主存。
    我们都用过代码仓库管理系统,多核系统的工作有点像一组程序员使用个代码仓库协作下工作。上述的双核系统有点像只有两个程序员。为了方便分别命名为Larry和Sergey。
    右边我们有一个中心仓库,这个代表着L2和RAM。Larry和Sergey分别各自在自己机器上完成工作,本地仓库这个代表着每个核私有的L1缓存。两者还有一些scratch area记录着寄存器以及栈上的局部变量。两个程序员修改着他们的本地仓库以及scratch area。他们做的任何工作都是依赖于当前看到的数据,这个很类似一个线程在一个核上运行的场景。
    随着Larry和Sergey不停的修改他们的本地仓库。pull和push到远程仓库在后台自动运行,并且在完全随机的时间里。一旦Larry修改了文件X,这个修改会push到远程仓库,但是不能保证何时push,可能立即push,也可能延迟push,可能他将会编辑其他文件Y,Z后在push。可能这些修改push到远程仓库比X更早。这种就类似stores操作重排。
    同样的,在Sergey的机器中,也不能保证时间以及顺序,这些修改会pull到本地仓库。这种就类比于loads操作重排。
    如果两个程序员分别使用两个独立的远程仓库开发,这些奇怪的push/pull操作并没有任何影响。这个就类比运行了2个独立的单线程程序。这种情况下,内存次序的基本规则是遵守的。
    会议上篇中的例子。X和Y都是全局变量,而且分别被初始化为0。
    假设X和Y是文件,存在于Larry和Sergey的本地仓库以及远程仓库中。在同一个时刻,Larry写入1到自己的本地仓库的X中,Sergey写入1到自己的本地仓库的Y。如果两者的修改都没有时间去push到远程和从远程pull。这个运行的结果就是r1=0 r2=0。这个好像挺违法直觉的,但是这个符合之前提到的代码控制策略。

内存屏障类型

幸运的是,Larry和Sergey并不是完全依赖不可预测随机的后台自动运行的pull和push的方式。他们还有一些特别的指令,被称为fence指令,这个就是内存屏障。在这个假设中,我们会有4种不同类型的内存屏障。每个类型的阻止不同类型的内存重排。根据命名规则就很容易理解。比如#storeLoad被设计着阻止一个store跟随一个load的内存重排。



大部分时候,真实的cpu指令一般都至少是上述屏障的组合或者附加了一些其他效果。但是一旦你理解了这4种类型的屏障,就很容易理解大多数cpu的屏障指令了或者高级语言中的屏障表达式了。

#LoadLoad

LoadLoad屏障保证了屏障前的Loads以及屏障后的Loads的顺序一致性。

在我们代码管理策略中,#LoadLoad指令就等于从远程仓库pull操作。
值得注意的是,#LoadLoad操作并不能保证获取最新的远程仓库的内容。可能pull了一个比较旧一点的仓库,但是至少和本地的值是一样的新的。

这个听起来有点“弱保证”,但是这个也仍然是个很好的方式去阻止获取“脏数据”。考虑之前的例子,当Sergey检查共享标志时,查看Larry是否有数据push。如果flag为true,在读取push的值之前,会添加一个LoadLoad屏障。

    if(IsPublished){
        COMPILER_BARRIER();
        return Value;
    }

显然的,这个例子依赖于IsPublished,在Sergey的本地仓库中是否从远程pull了。什么时候pull并不重要,但是一旦IsPublished值被更新了,加入#LoadLoad就能保证,Value值的读取必须更晚于这个标记本身。

#StoreStore

StoreStore屏障会阻止Store和Store指令间的重排。

在我们的假设中,StoreStore fence指令协作了push到远程仓库的操作。
作为一个额外的转折,让我们假设#StoreStore指令都不是即时的。他们都是有延迟执行的,异步的方式。即使Larry执行了#StoreStore,我们也不能认为,他push的修改立马就可以到达远程仓库。

这种弱保障的方式确实有一些“疑惑”,但是这里仍然能很好的阻止Sergey pull任何脏数据(Larry push的)。Larry只是需要push数据到共享内存,添加#StoreStore屏障,然后把IsPubulished标记设置成true。
这个屏障的加上后,不能保证其他用户立即获取到最新的Value值,但是保证了,远程仓库中一旦IsPublished值更新后,Value的值也一定是更新后的值。这里指的脏数据的情况是“远程仓库中IsPubulished值更新了,但是Value还是个旧值”。

    Value = x;
    COMPILER_BARRIER();
    IsPublished = 1;

同样的,我们观察IsPublished值从Larry的本地仓库流向Sergey的本地仓库。一旦Sergey看到了值修改了,他可以立马获取到一个正确的Value值。有趣的是,甚至Value的值都不需要是原子性的,甚至是一个结构体都可以。这个就可以理解“屏障的意思了”,结构体的store就是多条指令的store,意味着屏障不仅保障上下两条指令的顺序,而是保证“上半集合”和“下半集合”的顺序先后关系。集合内部的顺序并不保障。

#LoadStore

不像#LoadLoad和#StoreStore,#LoadStore并没有很好的假想场景在源码管理器中。最好的方式去理解#LoadStore,最好的方式去理解#LoadStore就是依据指令重排。
想象下,Larray有一堆指令去跟踪,一些指令是从本地仓库中load数据到寄存器中,一些是store寄存器数据到本地仓库中。Larry只有在特殊的情况下才有辨别这些指令的能力。有时他遇到了一个load,他接下来希望看到有一些stores接着后面,如果strore和load完全没有任何关系的时候,他就会被允许把store放到load前面去。这种情况下,存储一致性的基本准则还是遵循的,单线程下不修改其最后结果。
在真实的cpu中,一些指令会发生在load时cache Miss,而紧接着的Store cache Hit。这个也容易理解,cache Miss会多花几十倍的时钟周期去完成执行,cpu不可能等待着。但是就理解这个比喻而言,硬件细节并不重要。我们可以说Larry工作类了,在这仅有的时间内Larry会创造性的工作。何时以及到底会不会这么做都是不可预测的。幸运的是,这里有个相对低消耗的方式去阻止指令重排。当Larry遇到了#LoadStore时,他会克制重排,在屏障上下。
在我们的假设中,哪怕加入了#LoadLoad或者#StoreStore屏障,Larry还是会执行重排(因为没有加#LoadStore屏障)。然而,一个真实的cpu指令,一个有#LoadStore屏障功能的指令,至少含有上述两类屏障的功能。

#StoreLoad

StoreLoad屏障保障了,屏障前的store指令被执行后数据更新到其他处理器后,然后在进行load获取最新值。换句话说,这个有效阻止了屏障前的store操作和屏障后的load操作的重排。
` #StoreLoad很特殊,这个是仅有的方式去阻止r1=r2=0,在这个https://github.com/iamjokerjun/memReordering
这个时候可能会想到StoreLoad屏障和StoreStore屏障和LoadLoad屏障的组合有什么不同的。毕竟StoreStore屏障会push改变到远程仓库,而LoadLoad屏障会从远程仓库pull。然后仅有这两种屏障是不够的。记住push操作可能会延迟的。这个就是意味着PowerPC的lwsync指令-具有LoadLoad屏障,LoadStore屏障以及StoreStore屏障,但是没有StoreLoad屏障的功能,所以不足以阻止r1=r2=0.
就此类比而言,一个StoreLoad屏障push本地任何修改到远程仓库的,然后等到操作完成后,在拉领取相对来说最新的远程仓库的副本。在大多数处理器中,实现StoreLoad屏障相对来说会比其他类型屏障更消耗一些。
如果我们增加LoadStore屏障在上述操作中,也没什么大不了的,就意味着全内存屏障,4个屏障在一起。

相关文章

  • 浅谈运行期间cpu指令重排

    上篇谈到了编译器会进行内存操作指令的重排,这篇来谈谈运行期间cpu进行内存操作指令重排。当且仅当lock-free...

  • 浅谈编译期间的内存指令次顺问题

    在cpu运行指令之前,内存操作指令都可能在遵循一定的规则下被重排,编译期间以及cpu执行期间,主要目的都是为了使得...

  • JAVA之线程间如何通信(五)

    上节说了CPU缓存和内存屏障,CPU厂家考虑到指令重排的一些解决方案吧,本次说说线程通信,多个线程运行期间,它们之...

  • CPU缓存和内存屏障

    CPU性能优化手段 缓存 运行时指令重排 缓存 为了提高程序运行的性能,处理器大多会利用缓存来提高性能,而避免访问...

  • 指令重排序和内存屏障

    一:指令重排序 指令乱序有两种情况,一种是编译器做的优化,另外一种就是cpu流水线操作指令的延迟性。指令重排序是指...

  • volatile解决Java指令重排的问题

    浪费时间是一桩大罪过。——卢梭 什么是指令重排呢? 指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU...

  • 指令重排

    指令重排 指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度...

  • C++11 memory order

    为什么要momory order 线程1 可能发生指令重排,导致线程2断言失败 1.现代CPU保证单个CPU看到自...

  • Java指令重排序

    Java指令重排序Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重...

  • 指令重排演示

    什么是指令重排? 最终解析成cpu可以可以执行的指令,cpu去执行时,x=1不一定会先执行,因为不存在先后的逻辑关...

网友评论

    本文标题:浅谈运行期间cpu指令重排

    本文链接:https://www.haomeiwen.com/subject/xgyvrctx.html