JIT可以在某些表达式中将两个易失性读取合并为一个吗?

假设我们有一个volatile int a 。 一个线程呢

 while (true) { a = 1; a = 0; } 

而另一个线程呢

 while (true) { System.out.println(a+a); } 

现在,JIT编译器发出对应于2*a而不是a+a汇编是不合法 a+a吗?

一方面,易失性读取的目的是它应该始终从内存中消失。

另一方面,两个读取之间没有同步点,所以我看不出primefaces地处理a+a是违法的,在这种情况下我看不出像2*a这样的优化会如何破坏规范。

参考JLS将不胜感激。

简短回答:

是的,允许这种优化。 折叠两个顺序读取操作会产生序列的可观察行为,这些行为是primefaces的 ,但不会显示为操作的重新排序 。 在单个线程执行线程上执行的任何动作序列都可以作为primefaces单元执行。 通常,很难确保一系列操作以primefaces方式执行,并且很少会导致性能提升,因为大多数执行环境会引入开销以primefaces方式执行项目。

在原始问题给出的示例中,所讨论的操作顺序如下:

 read(a) read(a) 

primefaces地执行这些操作可确保第一行读取的值等于第二行读取的值。 此外,它表示在第二行读取的值是执行第一次读取时a中包含的值(反之亦然,因为primefaces读取操作根据程序的可观察执行状态同时发生) 。 所讨论的优化,即重用第二次读取的第一次读取的值,等同于primefaces地执行序列的编译器和/或JIT,因此是有效的。


原来更长的答案:

Java内存模型使用先前发生的部分排序来描述操作。 为了表示不能折叠a的第一个读取r1和第二个读取r2的限制,需要表明在它们之间出现某些语义操作。

r1r2的线程操作如下:

 --> r(a) --> r(a) --> add --> 

为了表达在r1r2之间存在某些东西(比如y )的要求,你需要要求r1 y y 发生之前 – 在 r2 之前发生 。 实际上,没有规则将读操作出现在先发生关系的左侧。 你可以得到的最接近的是 r2 之前y 发生 ,但是部分顺序允许y也在r1之前发生,从而使读取操作崩溃。

如果不存在需要r1r2之间进行操作的场景,则可以声明r1r2之间不会出现任何操作 ,并且不会违反语言所需的语义。 使用单个读取操作将等同于此声明。

编辑我的答案正在被拒绝,所以我将进入更多细节。

以下是一些相关问题:

  • 是否需要Java编译器或JVM来折叠这些读取操作?

    否。在add表达式中使用的表达式aa不是常量表达式,因此不要求它们被折叠。

  • JVM 是否会折叠这些读取操作?

    对此,我不确定答案。 通过编译程序并使用javap -c ,很容易看出Java编译器不会崩溃这些读操作。 不幸的是,certificateJVM不会崩溃操作(甚至更难处理,处理器本身)并不容易。

  • JVM 是否应该解除这些读取操作?

    可能不是。 每次优化都需要时间来执行,因此在分析代码所需的时间与您期望获得的收益之间存在平衡。 一些优化(例如数组边界检查消除或检查空引用)已被certificate对实际应用程序具有广泛的好处。 这种特殊优化有可能提高性能的唯一情况是两个相同的读操作顺序出现的情况。

    此外,如对该答案的响应以及其他答案所示,该特定变化将导致用户可能不期望的某些应用程序的意外行为改变。

编辑2:关于Rafael对两个无法重新排序的读取操作的声明的描述。 此语句旨在强调以下事实缓存a的读取操作可能会产生不正确的结果:

 a1 = read(a) b1 = read(b) a2 = read(a) result = op(a1, b1, a2) 

假设最初ab的默认值为0.那么你只执行第一次read(a)

现在假设另一个线程执行以下序列:

 a = 1 b = 1 

最后,假设第一个线程执行read(b)read(b) 。 如果要缓存最初读取的a值,最终会得到以下调用:

 op(0, 1, 0) 

这是不正确的。 由于在写入b之前存储了a的更新值,因此无法读取值b1 = 1 然后读取值a2 = 0 。 如果没有缓存,正确的事件序列将导致以下调用。

 op(0, 1, 1) 

但是,如果您要问“是否有任何方法允许读取a被缓存?”,答案是肯定的。 如果您可以将第一个线程序列中的所有三个读取操作作为primefaces单元执行 ,则允许缓存该值。 虽然跨多个变量进行同步很困难并且很少提供机会优化优势,但是当然可以想到遇到exception。 例如,假设ab各为4个字节,它们按顺序出现在内存中,并在8字节边界上对齐。 64位进程可以将序列read(a) read(b)为primefaces64位加载操作,这将允许缓存a的值(有效地将所有三个读取操作视为primefaces操作而不是仅仅前两个)。

在我的原始答案中,我反对建议的优化的合法性。 我主要从JSR-133 cookbook的信息中支持这一点,其中声明不能使用另一个易失性读取重新排序易失性读取 ,并且它进一步指出缓存读取将被视为重新排序。 然而,后面的陈述是以一些含糊不清的方式制定的,这就是为什么我经历了JMM的正式定义 ,我没有找到这样的指示。 因此,我现在认为允许优化。 然而,JMM非常复杂,本页的讨论表明,对于forms主义有更透彻理解的人可能会对这个角落的情况做出不同的决定。

表示线程1执行

 while (true) { System.out.println(a // r_1 + a); // r_2 } 

线程2执行:

 while (true) { a = 0; // w_1 a = 1; // w_2 } 

两个读取r_i和两个写入的w_i 动作a volatile (JSR 17.4.2)。 它们是外部操作,因为变量a用于多个线程中。 这些操作包含在所有操作A的集合中。 存在所有同步动作的总顺序,该同步顺序线程1线程2的 程序顺序一致(JSR 17.4.4)。 根据同步 –部分顺序的定义,在上面的代码中没有为此顺序定义边。 因此, before-before命令仅反映每个线程的线程内语义 (JSR 17.4.5)。

有了这个,我们将W定义为写看函数 ,其中W(r_i) = w_2值写函数 V(w_i) = w_2 (JLS 17.4.6)。 我采取了一些自由并取消了w_1因为它使这个正式certificate的轮廓更简单。 问题在于提议的执行E是否格式正确 (JLS 17.5.7)。 建议的执行E遵循线程内语义,在一致之前发生,遵循synchronized-with顺序,并且每次读取都遵循一致的写入。 检查因果关系要求是微不足道的(JSR 17.4.8)。 我不明白为什么非终止执行的规则是相关的,因为循环涵盖了整个讨论的代码(JLS 17.4.9),我们不需要区分可观察的行为

尽管如此,我还是找不到为什么禁止这种优化的任何迹象。 然而,它不适用于HotSpot VM的volatile读取,因为可以使用-XX:+PrintAssembly进行观察。 我认为性能优势很小,通常不会观察到这种模式。

备注:在观察Java内存模型语用 (多次)之后,我很确定,这种推理是正确的。

稍微修改了OP问题

  volatile int a //thread 1 while (true) { a = some_oddNumber; a = some_evenNumber; } // Thread 2 while (true) { if(isOdd(a+a)) { break; } } 

如果上面的代码已经顺序执行,那么就存在一个有效的顺序一致执行,它会在循环时中断thread2

然而,如果编译器 优化a + a到2a,那么thread2 while循环将永远不存在

因此,如果是顺序执行代码,则上述优化将禁止一个特定的执行。

主要问题,这个优化是一个问题吗?

 Q. Is the Transformed code Sequentially Consistent. 

答。 如果程序以顺序一致的方式执行,则没有数据争用,则程序正确同步。 参见JLS第17章的例17.4.8-1

  Sequential consistency: the result of any execution is the same as if the read and write operations by all processes were executed in some sequential order and the operations of each individual process appear in this sequence in the order specified by its program [Lamport, 1979]. Also see http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.3 

顺序一致性是一个有力的保证。 编译器将a + a优化为2a的执行路径也是有效的顺序一致执行 。 答案是肯定的。

  Q. Is the code violates happens before guarantees. 

答。 顺序一致性意味着在保证有效之前发生。 答案是肯定的。 JLS参考

所以我认为至少在OP情况下,优化在法律上是无效的。 线程2 while循环插入infinte的情况也很可能没有编译器转换。

一方面,易失性读取的目的是它应该始终从内存中消失。

这不是Java语言规范定义volatile的方式。 JLS简单地说:

对易失性变量v (第8.3.1.4节)的写入与任何线程对v的所有后续读取同步 (其中“后续”根据同步顺序定义)。

因此,对volatile变量的写入发生在该同一变量的任何后续读取之前 (并且可见)。

对于不是后续的读取,这个约束非常满足。 也就是说,如果已知在写入之后发生读取,则volatile仅确保写入的可见性。

您的计划不是这种情况。 对于每个观察到a为1的良好执行,我可以构造另一个良好形成的执行,其中a被观察为0,只是在写入之后移动读取。 这是可能的,因为before-before关系如下所示:

 write 1 --> read 1 write 1 --> read 1 | | | | | vv | v --> read 1 write 0 v write 0 | vs. | --> read 0 | | | | vvvv write 1 --> read 1 write 1 --> read 1 

也就是说,你的程序的所有JMM保证是a + a将产生0,1或2.如果a + a总是产生0,则满足。正如操作系统被允许在单个核上执行该程序一样,并且总是在循环的相同指令之前中断线程1,允许JVM重用该值 – 毕竟,可观察的行为保持不变。

通常,在写入之间移动读取违反了一致性,因为一些其他同步操作“在路上”。 在没有这种中间同步动作的情况下,可以从高速缓存满足易失性读取。

如其他答案中所述,有两个读取和两个写入。 想象一下以下执行(T1和T2表示两个线程),使用与下面的JLS语句匹配的注释:

  • T1: a = 0 //W(r)
  • T2: read temp1 = a //r_initial
  • T1: a = 1 //w
  • T2: read temp2 = a //r
  • T2: print temp1+temp2

在一致的环境中,这绝对是一种可能的线程交错。 那么你的问题是:是否允许JVM使r观察W(r)并读取0而不是1?

JLS#17.4.5规定:

如果对于A中的所有读取r,其中W(r)是r看到的写入动作,那么一组动作A发生 – 在一致之前,不是hb(r,W(r))或那里的情况在A中存在写w,使得wv = rv和hb(W(r),w)和hb(w,r)。

您建议的优化( temp = a; print (2 * temp); )将违反该要求。 因此,只有在r_initialr之间没有中间写入时,您的优化才能起作用,这在典型的multithreading框架中无法保证。

作为附带注释,请注意,无法保证从读取线程中可以看到写入需要多长时间。 参见例如: volatile的可见性及时性的详细语义 。