Java volatile环境下JIT生成的x86输出分析
我写这篇文章是关于 现在,我正在分析JIT为上述代码生成的内容。从我上一篇文章中的讨论中,我们知道输出Java volatile环境下JIT生成的x86输出分析,java,jvm,volatile,memory-barriers,Java,Jvm,Volatile,Memory Barriers,我写这篇文章是关于 现在,我正在分析JIT为上述代码生成的内容。从我上一篇文章中的讨论中,我们知道输出1,0是不可能的,因为: 写入volatilev会导致在v之前的每个动作a都会导致a在v之前可见(将刷新到内存) 我是否正确理解它之所以能工作是因为x86无法进行StoreStore重新排序?如果可能的话,它将需要额外的内存屏障,是吗 根据@Eugene的回答进行编辑: 在这里,我明白你的意思了——很清楚:下面的每个动作(之后)volatile read(int tmp=I)不会被重新排序
1,0
是不可能的,因为:
写入volatile
v
会导致在v
之前的每个动作a
都会导致a
在v
之前可见(将刷新到内存)
我是否正确理解它之所以能工作是因为x86无法进行
StoreStore
重新排序?如果可能的话,它将需要额外的内存屏障,是吗
根据@Eugene的回答进行编辑: 在这里,我明白你的意思了——很清楚:
下面的每个动作(之后)
volatile read(int tmp=I
)不会被重新排序
这里,你再加一道屏障。它确保不会使用int tmp=i
对任何操作重新排序。但是,为什么它很重要?为什么我有怀疑?据我所知,volatile load
保证:
在易变负载可见之前,易变负载之后的每个动作都不会被重新排序
我看到你在写:
需要有顺序一致性
但是,我不明白为什么需要顺序一致性。有几件事,首先
将被刷新到内存中
——这是非常错误的。它几乎从不刷新主内存—它通常会将StoreBuffer消耗到L1
,所有缓存之间的数据同步取决于缓存一致性协议,但如果您更容易理解这些术语中的概念,那也没关系—只需知道这有点不同,速度更快
这是一个很好的问题,为什么[StoreLoad]
确实存在,也许这会让事情变得更清楚一点<代码>易失性实际上是关于围栏的,下面是一个示例,说明在一些易失性操作的情况下会插入哪些屏障。例如,我们有一个易失性负载
:
// i is some shared volatile field
int tmp = i; // volatile load of "i"
// [LoadLoad|LoadStore]
注意这里的两个屏障LoadStore
和LoadLoad
;在简单的英语中,这意味着任何负载
和存储
在易失性负载/读取
之后不能“向上”移动屏障,不能在该易失性负载的“上方”重新排序
下面是易失性存储
的示例
// "i" is a shared volatile variable
// [StoreStore|LoadStore]
i = tmp; // volatile store
这意味着任何加载
和存储
都不能“低于”加载存储本身
这基本上建立了“先发生后发生”关系,volatile load
是获取负载,volatile store
是释放存储(这也与store
和load
cpu缓冲区的实现方式有关,但这几乎超出了问题的范围)
如果你想一想,它对我们所知道的volatile
一般来说是非常有意义的;它说,一旦易失性负载观察到易失性存储,也会观察到易失性存储之前的所有内容,这与内存屏障相当。现在,当一个易失性存储发生时,上面的所有内容都不能超出它,并且一旦发生了易失性负载,下面的所有内容都不能超出它,否则之前发生的所有内容都将被破坏
但不是这样,还有更多。需要有顺序一致性,这就是为什么任何sane实现都将保证挥发物本身不会被重新排序,因此又插入了两个围栏:
// 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个是空闲的,因为它是强内存模型。唯一需要实现的是StoreLoad
。在其他CPU上,例如ARM
,lwsyn
是使用的一条指令,但我对它们知之甚少
通常,mfence
对于x86
上的StoreLoad
是一个很好的选择,但是通过lock add
(以更便宜的方式AFAIK)可以保证这一点,这就是为什么您会在那里看到它。基本上,这就是StoreLoad
屏障。是的-你在最后一句话中说得对,对于一个较弱的内存模型-需要StoreStore
屏障。另一方面,当您通过构造函数中的final
字段安全地发布引用时,会用到这一点。退出构造函数后,将插入两个围栏:LoadStore
和storestorestore
// "i" is a shared volatile variable
// [StoreStore|LoadStore]
i = tmp; // volatile store
恕我直言——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 load之后的每个操作在volatile load可见之前都不会被重新排序。volatile本身不可能重新排序-其他操作可以自由重新排序。让我举个例子:
int tmp = i; // volatile load of a shared variable "i"
// [LoadStore|LoadLoad]
int x = 3; // plain store
int y = 4; // plain store
最后两个操作x=3
和y=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
有几件事,首先将被刷新到内存中
——这是非常错误的。它几乎从不刷新主内存-它通常会将StoreBuffer消耗到L1
,所有缓存之间的数据同步取决于缓存一致性协议,但如果您更容易理解这一概念,请参阅
[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]
int tmp = i; // volatile load of a shared variable "i"
// [LoadStore|LoadLoad]
int x = 3; // plain store
int y = 4; // plain store
int tmp = i; // volatile load
// [LoadStore|LoadLoad]
// see how they have been inverted here...
int y = 4; // plain store
int x = 3; // plain store