【问题标题】:Is there any instruction reordering done by the Hotspot JIT compiler that can be reproduced?Hotspot JIT 编译器是否有任何可以重现的指令重新排序?
【发布时间】:2016-06-23 08:11:53
【问题描述】:

我们知道,一些 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 编译器是否有任何可以重现的指令重新排序?


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

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 对象已发布,至少有 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

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


更新2: @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

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

【问题讨论】:

  • 您打算如何检测到这种情况发生?绝对不能允许任何此类优化泄漏到“用户领域”。
  • @Thilo A) 通过查看 HotSpot 源代码? B) java 内存模型允许许多重新排序,因此如果发生这种情况,可以从不同的线程中观察到这种情况
  • 一般情况下,方法A)比较实用,我们可以观察JIT编译器生成的汇编代码。
  • @Thilo 是的 - 除非两个线程正确同步到构造的对象,否则另一个线程可能会看到处于部分构造状态的对象。无论是因为内存可见性(CPU 缓存等)还是因为 CPU 或 JIT 编译器(或其他任何东西)重新排序,都没有指定(在 JLS 的 JMM 部分中)。除非线程正确同步,否则 JVM 不需要保护程序免受这种明显的重新排序。 (您是否真的会在实践中看到这种重新排序是另一回事)
  • 当然,我提到的重新排序是有效的,很抱歉错字说它是无效的。我只想重现由 JIT 编译器完成的重新排序,如果给定一个未正确同步的程序,它会影响另一个线程。

标签: java multithreading jvm jit jvm-hotspot


【解决方案1】:

您可以重现任何编译器重新排序。正确的问题是——为此使用哪种工具。为了查看编译器重新排序 - 您必须使用 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;
    }
}

请注意,数组访问是无序的。在我的机器上,带有全局变量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 options + ideal graph visualizer): 其中节点的输入是节点操作的输入。每个节点根据其输入和操作定义一个值,并且该值在所有输出边上都可用。很明显,编译器看不到指针和整数存储节点之间的任何区别,因此唯一限制它的就是内存屏障。结果,为了减少寄存器压力,目标代码大小或其他编译器决定以这个奇怪(从用户的角度来看)顺序在基本块内调度指令。您可以使用以下选项(在 fastdebug build 中可用)在 Hotspot 中使用指令调度:-XX:+StressLCM-XX:+StressGCM

【讨论】:

  • 感谢@Ivan,对于 reorderLocal,重新排序不会影响任何其他线程,因为 sum 是一个局部变量,而 的内容数组 不变。我认为重现可能影响另一个线程的重新排序更有价值(当然,这里我们假设程序本身没有正确同步)。
  • 我也做了测试,但生成的汇编代码看起来没有编译器重新排序。有关详细信息,请参阅我对问题的更新。
  • @user2351818 这是因为你打印错误的方法,使用-XX:CompileCommand="print org.openjdk.jcstress.tests.unsafe.generated.UnsafePublication_jcstress*::call" 。构造函数和发布方法被内联编译成调用方法
  • 您能否解释一下通过这种重新排序我们可以获得哪些性能优势(我的意思是问题中讨论的这种特定重新排序)?
  • 惊人的答案!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-02
  • 2019-12-04
  • 1970-01-01
相关资源
最近更新 更多