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

时间:2016-03-09 05:08:53

标签: java multithreading jvm jit jvm-hotspot

众所周知,一些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是否有任何可以重现的指令重新排序?

更新:  我使用以下命令在我的机器上执行test(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可接受对象已发布,至少有一个字段可见。

这意味着观察者线程看到了 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

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

1 个答案:

答案 0 :(得分:16)

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

让我们考虑基于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;
    }
}

请注意,阵列访问是无序的。在我的机器上,对于具有全局变量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,类库和硬件中的并发支持的正确性。 Here是一个复制器,指令调度程序可以决定发出一些字段存储,然后发布引用,然后发出其余的字段存储(也可以阅读安全发布和指令调度here )。在我的机器上使用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图来模拟数据和控制流(您可以阅读有关基于图形的中间表示here)。下图显示了我们示例的IR图(-XX:+PrintIdeal -XX:PrintIdealGraphLevel=1 -XX:PrintIdealGraphFile=graph.xml选项+ ideal graph visualizer): enter image description here 其中节点的输入是节点操作的输入。每个节点根据它的输入和操作定义一个值,该值在所有输出边缘都可用。很明显,编译器看不到指针和整数存储节点之间的任何区别,因此唯一限制它的是内存屏障。因此,为了降低寄存器压力,目标代码大小或其他编译器决定在此奇怪(从用户的角度)的顺序中在基本块内安排指令。您可以使用以下选项(在fastdebug构建中提供)在Hotspot中使用指令调度:-XX:+StressLCM-XX:+StressGCM