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

我正在写这篇文章与深入理解Java中的volatile有关

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为上面的代码生成了什么。 根据我在上一篇文章中的讨论,我们知道输出1, 0是不可能的,因为:


写入volatile v会导致v每个操作都会导致av可见之前可见(将被刷新到内存中)。


  .................(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 )的every action below (after)都不会被重新排序。

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

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

易失性负载可见之前,不会重新排序易失性负载后的每个动作。

我看到你写道:

需要有连续的一致性

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

有几件事情,首先will be flushed to memory – 这是非常错误的。 它几乎从不与主存储器冲洗 – 它通常将StoreBuffer排到L1并且由高速缓存一致性协议来同步所有缓存之间的数据, 但是如果你更容易理解这些概念,那就很好 – 只是知道这有点不同而且更快。

这是一个很好的问题,为什么[StoreLoad]确实在那里,可能这会清理一些事情。 volatile确实都是关于围栏的。 以下是将要发生的事情的示例:

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

对于volatile load发生的情况以及对于易失性存储会发生的情况:

  // [StoreStore] // [LoadStore] i = tmp; // volatile store 

但那不是它,还有更多。 需要有一个sequential consistency ,这就是为什么任何理智的实现将保证volatile本身不被重新排序,因此插入了两个更多的围栏:

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

还有一个:

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

现在,事实certificate,在x86 ,4个内存屏障中有3个是免费的 – 因为它是一个strong memory model 。 唯一需要实现的是StoreLoad

通常 StoreLoad对于x86上的mfence是个不错的选择,但是通过lock add可以保证同样的东西,这就是你在那里看到它的原因。 基本上就是 StoreLoad屏障。 是的 – 你的最后一句话是正确的,对于较弱的内存模型 – 将需要StoreStore屏障。 在旁注上,这是通过构造函数中的final字段安全地发布引用时使用的内容。 退出构造函数后,插入了两个fence: LoadStoreStoreStore

编辑

假设你有这种情况:

 [StoreStore] [LoadStore] int x = i; // volatile store int j = 3; // plain actions j = x; // volatile load [LoadLoad] [LoadStore] 

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

Every action after volatile load won't be reordered before volatile load is visible你有点遗漏了这一点btw(如果我没有记错)通过Every action after volatile load won't be reordered before volatile load is visible易失性本身无法重新排序 – 其他操作可以自由重新排序。 让我举一个例子:

  int y = 0; int tmp = i; // volatile load // [LoadStore] // [LoadLoad] int x = 3; // plain load y = 4; // plain store 

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

  int y = 0; int tmp = i; // volatile load // [LoadStore] // [LoadLoad] // see how they have been inverted here... y = 4; // plain store int x = 3; // plain load