在 volatile 上下文中分析 JIT 生成的 x86 输出

Analyzing of x86 output generated by JIT in the context of volatile

我写这篇 post 是为了

public class Main {
    private int x;
    private volatile int g;


    public void actor1(){
       x = 1;
       g = 1;
    }


    public void actor2(){
       put_on_screen_without_sync(g);
       put_on_screen_without_sync(x);
    }
}

现在,我正在分析JIT为上面这段代码生成了什么。从我们之前 post 的讨论中我们知道输出 1, 0 是不可能的,因为:


写入易失性 v 导致 av 之前的每个操作导致 a 在 [=15= 之前可见(将被刷新到内存) ] 将可见。


   .................(I removed not important body of method).....

  0x00007f42307d9d5e: c7460c01000000     (1) mov       dword ptr [rsi+0ch],1h
                                                ;*putfield x
                                                ; - package.Main::actor1@2 (line 14)

  0x00007f42307d9d65: bf01000000          (2) mov       edi,1h
  0x00007f42307d9d6a: 897e10              (3) mov       dword ptr [rsi+10h],edi
  0x00007f42307d9d6d: f083042400          (4) lock add  dword ptr [rsp],0h
                                                ;*putfield g
                                                ; - package.Main::actor1@7 (line 15)

  0x00007f42307d9d72: 4883c430            add       rsp,30h
  0x00007f42307d9d76: 5d                  pop       rbp
  0x00007f42307d9d77: 850583535116        test      dword ptr [7f4246cef100h],eax
                                                ;   {poll_return}
  0x00007f42307d9d7d: c3                  ret

我是否正确理解它的工作原理是因为 x86 无法进行 StoreStore 重新排序?如果可以的话,它需要额外的内存屏障,是吗?


在出色的@Eugene 回答后编辑:

 int tmp = i; // volatile load
 // [LoadStore]
 // [LoadLoad]

在这里,我明白你的意思了——很明显:every action below (after) volatile read (int tmp = i) 不会被重新排序。

 // [StoreLoad] -- this one
 int tmp = i; // volatile load
 // [LoadStore]
 // [LoadLoad]

在这里,你又放了一道屏障。它确保我们不会使用 int tmp = i 重新排序任何操作。但是,为什么它很重要?为什么我有疑问?据我所知 volatile load 保证:

易变负载可见之前,不会对易变负载进行重新排序。

我看到你写了:

There needs to be a sequential consistency

但是,我不明白为什么需要顺序一致性。

有几件事,首先是 will be flushed to memory - 这是非常错误的。它几乎从不刷新到主内存 - 它通常将 StoreBuffer 耗尽到 L1 并且由缓存一致性协议来同步所有缓存之间的数据, 但是 如果它更容易为了让您用这些术语来理解这个概念,这很好 - 只知道略有不同且速度更快。

为什么 [StoreLoad] 确实存在,这是一个很好的问题,也许这会稍微澄清一些事情。 volatile 确实是关于栅栏的,这里是一个例子,说明在一些易变操作的情况下会插入什么障碍。例如我们有一个 volatile load:

  // i is some shared volatile field
  int tmp = i; // volatile load of "i"
  // [LoadLoad|LoadStore]

注意这里的两个障碍 LoadStoreLoadLoad;用简单的英语来说,这意味着 volatile load/read 之后的任何 LoadStore 不能 "move up" 屏障,它们不能被重新排序 "above"不稳定的负载。

这里是 volatile store 的例子。

 // "i" is a shared volatile variable
 // [StoreStore|LoadStore]
 i = tmp; // volatile store

表示任何LoadStore都不能去"below"加载存储本身。

这基本上建立了 happens-before 关系,volatile load 获取负载 volatile store 发布存储(这也与 StoreLoad cpu 缓冲区的实现方式有关,但这几乎超出了问题的范围)。

如果你仔细想想,这对我们所了解的 volatile 一般情况来说是完全合理的;它说一旦易失性存储被易失性负载观察到volatile store之前的所有内容也将被观察到,这与内存障碍相当。现在说得通了,当发生volatile store时,它上面的一切都不能超过它,一旦发生volatile load,它下面的一切都不能超过它,否则happens-before就会被破坏。

但是不是这样,还有更多。需要 顺序一致性 ,这就是为什么任何理智的实现都将保证 volatiles 本身不会重新排序,因此插入了两个栅栏:

 // any store of some other volatile
 // can not be reordered with this volatile load
 // [StoreLoad] -- this one
 int tmp = i; // volatile load of a shared variable "i"
 // [LoadStore|LoadLoad]

这里还有一个:

// [StoreStore|LoadStore]
i = tmp; // volatile store
// [StoreLoad] -- and this one

现在,事实证明在 x86 上,4 个内存屏障中有 3 个是空闲的 - 因为它是 strong memory model。唯一需要实施的是 StoreLoad。在其他 CPU 上,例如 ARMlwsycn 是使用的一条指令 - 但我对它们了解不多。

通常 mfencex86StoreLoad 的一个不错的选择,但通过 [=44= 可以保证同样的事情](AFAIK 以更便宜的方式),这就是为什么你在那里看到它。基本上 StoreLoad 障碍。是的 - 你的最后一句话是对的,对于较弱的记忆模型 - StoreStore 障碍是必需的。旁注是当您通过构造函数中的 final 字段安全发布引用时使用的内容。退出构造函数后,插入了两个栅栏:LoadStoreStoreStore.

对这一切持保留态度 - 只要 JVM 不违反任何规则,它就可以自由地忽略这些:Aleksey Shipilev 对此进行了精彩的讨论。


编辑

假设你有这种情况:

[StoreStore|LoadStore]
int x = 4; // volatile store of a shared "x" variable

int y = 3; // non-volatile store of shared variable "y"

int z = x; // volatile load
[LoadLoad|LoadStore]

基本上没有任何障碍可以阻止 volatile storevolatile load 一起重新排序(即:易失性负载将首先执行 ) 这显然会导致问题;因此违反了顺序一致性。

顺便说一句,您有点错过了这里的要点(如果我没记错的话)Every action after volatile load won't be reordered before volatile load is visible。无法对 volatile 本身进行重新排序 - 其他操作可以自由重新排序。让我举个例子:

 int tmp = i; // volatile load of a shared variable "i"
 // [LoadStore|LoadLoad]

 int x = 3; // plain store
 int y = 4; // plain store

最后两个操作x = 3y = 4完全可以自由重新排序,它们不能浮动在volatile之上,但是它们可以通过他们自己重新订购。上面的例子是完全合法的:

 int tmp = i; // volatile load
 // [LoadStore|LoadLoad]

 // see how they have been inverted here...
 int y = 4; // plain store
 int x = 3; // plain store