Hotspot JIT 编译器完成的任何指令重新排序是否可以重现?
Is there any instruction reordering done by the Hotspot JIT compiler that can be reproduced?
我们知道,有些 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
也就是说,step2和step3可以被JIT编译器重新排序。
尽管这在理论上是有效重新排序,但我无法在 x86 平台下使用 Hotspot(jdk1.7) 重现它。
那么,Hotspot JIT 编译器完成的任何指令重新排序是否可以重现?
更新:
我在我的机器上做了 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 ACCEPTABLE The object is published, at least 1 field is visible.
这意味着观察者线程看到了 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
这里似乎没有编译器重新排序。
更新2:
@Ivan 纠正了我。我使用了错误的 JIT 命令来捕获程序集 code.After 修复了这个错误,我可以捕获下面的汇编代码:
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(as it uses HotSpot's assembly log output) or 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 - an experimental harness and a suite of tests to aid the research in the correctness of concurrency support in the JVM, class libraries, and hardware. Here is a reproducer where the instruction scheduler may decide to emit a few field stores, then publish the reference, then emit the rest of the field stores(also you can read about safe publications and instruction scheduling 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 [=13=]x0,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). The following picture shows the IR graph for our example(-XX:+PrintIdeal -XX:PrintIdealGraphLevel=1 -XX:PrintIdealGraphFile=graph.xml
options + ideal graph visualizer):
其中节点的输入是节点操作的输入。每个节点根据其输入和操作定义一个值,并且该值在所有输出边上都可用。很明显,编译器看不到指针和整数存储节点之间的任何区别,因此唯一限制它的是内存屏障。因此,为了减少寄存器压力、目标代码大小或其他原因,编译器决定以这种 strange(从用户的角度来看)顺序在基本块内安排指令。您可以使用以下选项(在 fastdebug 构建中可用)在 Hotspot 中使用指令调度:-XX:+StressLCM
和 -XX:+StressGCM
.
我们知道,有些 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
也就是说,step2和step3可以被JIT编译器重新排序。 尽管这在理论上是有效重新排序,但我无法在 x86 平台下使用 Hotspot(jdk1.7) 重现它。
那么,Hotspot JIT 编译器完成的任何指令重新排序是否可以重现?
更新: 我在我的机器上做了 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 ACCEPTABLE The object is published, at least 1 field is visible.
这意味着观察者线程看到了 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
这里似乎没有编译器重新排序。
更新2: @Ivan 纠正了我。我使用了错误的 JIT 命令来捕获程序集 code.After 修复了这个错误,我可以捕获下面的汇编代码:
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(as it uses HotSpot's assembly log output) or 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 - an experimental harness and a suite of tests to aid the research in the correctness of concurrency support in the JVM, class libraries, and hardware. Here is a reproducer where the instruction scheduler may decide to emit a few field stores, then publish the reference, then emit the rest of the field stores(also you can read about safe publications and instruction scheduling 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 [=13=]x0,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). The following picture shows the IR graph for our example(-XX:+PrintIdeal -XX:PrintIdealGraphLevel=1 -XX:PrintIdealGraphFile=graph.xml
options + ideal graph visualizer):
-XX:+StressLCM
和 -XX:+StressGCM
.