假设我们有一个volatile int a。一线程做
volatile int a
while (true) { a = 1; a = 0; }
而另一个线程
while (true) { System.out.println(a+a); }
现在,JIT编译器发出与而不是相对应的程序集是否 非法 ?2*a``a+a
2*a``a+a
一方面,易失性读取的主要目的是应该始终从内存中读取数据。
另一方面,两次读取之间没有同步点,因此我看不到a+a原子地进行处理是非法的,在这种情况下,我看不到诸如此类的优化2*a将如何破坏规格。
a+a
2*a
对JLS的引用将不胜感激。
简短答案:
是的,允许这种优化。折叠两个顺序的读取操作将产生可观察到的 原子 序列行为,但不会表现为操作的 重新排序 。在单个执行线程上执行的任何动作序列都可以作为原子单元执行。通常,很难确保一系列操作以原子方式执行,并且很少会导致性能提高,因为大多数执行环境都会 引入开销 以原子方式执行项目。
在原始问题给出的示例中,所讨论的操作序列如下:
read(a) read(a)
原子地执行这些操作可确保第一行读取的值等于第二行读取的值。此外,这意味着在第二行读取的值是a在执行第一次读取时包含的值(反之亦然,因为根据程序的可观察执行状态,两个原子读取操作都同时发生)。所讨论的优化将第一读取的值重用于第二读取,该优化等效于编译器和/或JIT以原子方式执行该序列,因此是有效的。
a
原来较长的答案:
Java内存模型使用 在 部分排序 之前发生的事件来 描述操作。为了表达的限制,即第一次读r1和二读r2的a,不能折叠,则需要证明一些操作语义需要在它们之间出现。
r1
r2
使用r1和进行的线程操作r2如下:
--> r(a) --> r(a) --> add -->
为了表达的需要的东西(比如说y之间)的谎言r1和r2,你需要要求r1 的之前发生 y 和 y 之前发生 r2。碰巧的是,没有规则在“ 先发生后发生” 关系的左侧出现读取操作。您可以得到的最接近的说法是:y before-before r2,但是部分顺序也可以y发生在before之前r1,因此会破坏读取操作。
y
如果不存在 要求 运算符介于r1和之间的方案r2,那么您可以声明 没有运算符 出现在之间r1,r2并且不违反语言的必需语义。使用单个读取操作将等同于此声明。
编辑 我的答案被否决了,所以我将进一步探讨其他细节。
以下是一些相关问题:
否。在add表达式中使用的表达式a和a表达式不是常数表达式,因此不需要将它们折叠。
为此,我不确定答案。通过编译程序并使用javap -c,很容易看出Java编译器不会折叠这些读取操作。不幸的是,要证明JVM不会折叠操作(甚至更难于处理器本身)并不容易。
javap -c
可能不是。每次优化都需要花费时间才能执行,因此在分析代码所花费的时间与您期望获得的收益之间是一个平衡。事实证明,某些优化(例如数组边界检查消除或检查空引用)对现实应用程序具有 广泛的 好处。该特定优化可能会提高性能的唯一情况是两个相同的读取操作顺序出现的情况。
此外,如对此答案的响应以及其他答案所示,此特定更改将导致用户可能不希望的某些应用程序发生 意外的 行为更改。
编辑2: 关于拉斐尔(Rafael)的描述,它声称两个读取操作无法重新排序。该语句旨在突出显示以下事实,即a按以下顺序缓存的读取操作可能会产生错误的结果:
a1 = read(a) b1 = read(b) a2 = read(a) result = op(a1, b1, a2)
假设最初a并b有其默认值0。然后你执行只是第一read(a)。
b
read(a)
现在,假设另一个线程执行以下序列:
a = 1 b = 1
最后,假设第一个线程执行line read(b)。如果要缓存的原始读取值a,将以以下调用结束:
read(b)
op(0, 1, 0)
这是不正确的。由于更新后的值a是在写入之前存储的b,因此无法读取该值b1 = 1 ,然后再 读取该值a2 = 0。如果不进行缓存,则正确的事件顺序将导致随后的调用。
b1 = 1
a2 = 0
op(0, 1, 1)
但是,如果您要问“是否有任何方法a可以缓存读取的内容?”,答案是肯定的。如果您可以作为 原子单位 在第一个线程序列中执行 所有三个 读取操作,则可以缓存该值。虽然很难跨多个变量进行同步,并且很少能提供机会优化优势,但是可以肯定遇到异常。例如,假设和分别为4个字节,并且它们在内存中顺序出现,并在8字节边界上对齐。64位进程可以将序列实现为原子64位加载操作,这将允许 __a``b``a``read(a) read(b)``a被缓存(有效地将所有三个读取操作视为一个原子操作,而不仅仅是前两个)。
a``b``a``read(a) read(b)``a