分析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
每个操作都会导致a
在v
可见之前可见(将被刷新到内存中)。
.................(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: LoadStore
和StoreStore
。
编辑
假设你有这种情况:
[StoreStore] [LoadStore] int x = i; // volatile store int j = 3; // plain actions j = x; // volatile load [LoadLoad] [LoadStore]
基本上没有障碍可以阻止volatile store
与volatile 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 = 3
和y = 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