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