计算机系统

浅谈运行期间cpu指令重排

2019-07-29  本文已影响0人  Teech

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

内存屏障类型

幸运的是,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个屏障在一起。

上一篇下一篇

猜你喜欢

热点阅读