Hotspot JIT编译器是否可以重现任何指令重新排序?

我们知道,一些JIT允许重新排序对象初始化,例如,

someRef = new SomeObject(); 

可以分解为以下步骤:

 objRef = allocate space for SomeObject; //step1 call constructor of SomeObject; //step2 someRef = objRef; //step3 

JIT编译器可能会重新排序如下:

 objRef = allocate space for SomeObject; //step1 someRef = objRef; //step3 call constructor of SomeObject; //step2 

即,步骤2和步骤3可以由JIT编译器重新排序。 即使这在理论上是有效的重新排序,我也无法使用x86平台下的Hotspot(jdk1.7)重现它。

那么,Hotspot JIT comipler是否可以重现任何指令重新排序?


更新 :我使用以下命令在我的机器(Linux x86_64,JDK 1.8.0_40,i5-3210M)上进行了测试 :

 java -XX:-UseCompressedOops -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand="print org.openjdk.jcstress.tests.unsafe.UnsafePublication::publish" -XX:CompileCommand="inline, org.openjdk.jcstress.tests.unsafe.UnsafePublication::publish" -XX:PrintAssemblyOptions=intel -jar tests-custom/target/jcstress.jar -f -1 -t .*UnsafePublication.* -v > log.txt 

我可以看到该工具报告的内容如下:

[1] 5可接受对象已发布,至少有1个字段可见。

这意味着观察者线程看到了一个未初始化的MyObject实例。

但是,我没有看到像@ Ivan那样生成的汇编代码:

 0x00007f71d4a15e34: mov r11d,DWORD PTR [rbp+0x10] ;getfield x 0x00007f71d4a15e38: mov DWORD PTR [rax+0x10],r11d ;putfield x00 0x00007f71d4a15e3c: mov DWORD PTR [rax+0x14],r11d ;putfield x01 0x00007f71d4a15e40: mov DWORD PTR [rax+0x18],r11d ;putfield x02 0x00007f71d4a15e44: mov DWORD PTR [rax+0x1c],r11d ;putfield x03 0x00007f71d4a15e48: mov QWORD PTR [rbp+0x18],rax ;putfield o 

这里似乎没有编译器重新排序。


Update2 :@Ivan纠正了我。 我使用错误的JIT命令来捕获汇编代码。修复此错误后,我可以在汇编代码下面编写:

 0x00007f76012b18d5: mov DWORD PTR [rax+0x10],ebp ;*putfield x00 0x00007f76012b18d8: mov QWORD PTR [r8+0x18],rax ;*putfield o ; - org.openjdk.jcstress.tests.unsafe.generated.UnsafePublication_jcstress$Runner_publish::call@94 (line 156) 0x00007f76012b18dc: mov DWORD PTR [rax+0x1c],ebp ;*putfield x03 

显然,编译器进行了重新排序,导致了不安全的发布。

您可以重现任何编译器重新排序。 正确的问题是 – 用于此目的的工具。 为了查看编译器重新排序 – 您必须使用JITWatch (因为它使用HotSpot的程序集日志输出)或使用LinuxPerfAsmProfiler的JMH跟进到程序集级别。

让我们考虑基于JMH的以下基准:

 public class ReorderingBench { public int[] array = new int[] {1 , -1, 1, -1}; public int sum = 0; @Benchmark public void reorderGlobal() { int[] a = array; sum += a[1]; sum += a[0]; sum += a[3]; sum += a[2]; } @Benchmark public int reorderLocal() { int[] a = array; int sum = 0; sum += a[1]; sum += a[0]; sum += a[3]; sum += a[2]; return sum; } } 

请注意,arrays访问是无序的。 在我的机器上 ,具有全局变量sum汇编器输出的方法是:

 mov 0xc(%rcx),%r8d ;*getfield sum ... add 0x14(%r12,%r10,8),%r8d ;add a[1] add 0x10(%r12,%r10,8),%r8d ;add a[0] add 0x1c(%r12,%r10,8),%r8d ;add a[3] add 0x18(%r12,%r10,8),%r8d ;add a[2] 

但对于具有局部变量sum方法的方法,访问模式已更改:

 mov 0x10(%r12,%r10,8),%edx ;add a[0] <-- 0(0x10) first add 0x14(%r12,%r10,8),%edx ;add a[1] <-- 1(0x14) second add 0x1c(%r12,%r10,8),%edx ;add a[3] add 0x18(%r12,%r10,8),%edx ;add a[2] 

您可以使用c1编译器优化c1_RangeCheckElimination

更新:

从用户的角度来看,很难看到只有编译器重新排序,因为你必须运行数百万个样本来捕捉活泼的行为。 另外,分离编译器和硬件问题也很重要,例如,像POWER这样的弱排序硬件可以改变行为。 让我们从正确的工具开始: jcstress - 一个实验性工具和一套测试,以帮助研究JVM,类库和硬件中的并发支持的正确性。 这是一个重现器,指令调度程序可以决定发出一些字段存储,然后发布引用,然后发出其余的字段存储(也可以在这里阅读安全发布和指令调度)。 在我的机器上使用Linux x86_64,JDK 1.8.0_60,i5-4300M编译器生成以下代码:

 mov %edx,0x10(%rax) ;*putfield x00 mov %edx,0x14(%rax) ;*putfield x01 mov %edx,0x18(%rax) ;*putfield x02 mov %edx,0x1c(%rax) ;*putfield x03 ... movb $0x0,0x0(%r13,%rdx,1) ;*putfield o 

但有时候:

 mov %ebp,0x10(%rax) ;*putfield x00 ... mov %rax,0x18(%r10) ;*putfield o <--- publish here mov %ebp,0x1c(%rax) ;*putfield x03 mov %ebp,0x18(%rax) ;*putfield x02 mov %ebp,0x14(%rax) ;*putfield x01 

更新2:

关于绩效福利的问题。 在我们的例子中,这种优化(重新排序)并没有带来有意义的性能优势,它只是编译器实现的副作用。 HotSpot使用sea of nodes来建模数据和控制流程(您可以在这里阅读基于图形的中间表示)。 下图显示了我们示例的IR图( -XX:+PrintIdeal -XX:PrintIdealGraphLevel=1 -XX:PrintIdealGraphFile=graph.xml选项+ 理想图形可视化器 ): 在此处输入图像描述 其中节点的输入是节点操作的输入。 每个节点根据其输入和操作定义一个值,该值在所有输出边缘都可用。 很明显,编译器看不到指针和整数存储节点之间的任何区别,因此唯一限制它的是内存屏障。 因此,为了降低寄存器压力,目标代码大小或其他编译器决定在这个奇怪的 (从用户的角度)顺序中调度基本块内的指令。 您可以使用以下选项(在fastdebug构建中提供)在Hotspot中使用指令调度: -XX:+StressLCM-XX:+StressGCM