草庐IT

java - Hotspot JIT 编译器完成的任何指令重新排序是否可以重现?

coder 2023-08-28 原文

正如我们所知,一些 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命令来捕获汇编代码。修复此错误后,我可以捕获以下汇编代码:

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 的汇编日志输出)或 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 options + ideal graph visualizer ): 其中节点的输入是节点操作的输入。每个节点根据其输入和操作定义一个值,并且该值在所有输出边上都可用。很明显,编译器看不到指针和整数存储节点之间的任何区别,因此唯一限制它的是内存屏障。因此,为了减少寄存器压力、目标代码大小或其他原因,编译器决定以这种奇怪(从用户的角度来看)顺序在基本 block 中安排指令。您可以使用以下选项(在 fastdebug 构建中可用)在 Hotspot 中进行指令调度:-XX:+StressLCM-XX:+StressGCM

关于java - Hotspot JIT 编译器完成的任何指令重新排序是否可以重现?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35883354/

有关java - Hotspot JIT 编译器完成的任何指令重新排序是否可以重现?的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  3. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  4. ruby - 我可以使用 Ruby 从 CSV 中删除列吗? - 2

    查看Ruby的CSV库的文档,我非常确定这是可能且简单的。我只需要使用Ruby删除CSV文件的前三列,但我没有成功运行它。 最佳答案 csv_table=CSV.read(file_path_in,:headers=>true)csv_table.delete("header_name")csv_table.to_csv#=>ThenewCSVinstringformat检查CSV::Table文档:http://ruby-doc.org/stdlib-1.9.2/libdoc/csv/rdoc/CSV/Table.html

  5. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

    我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

  6. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  7. ruby-on-rails - link_to 不显示任何 rails - 2

    我试图在索引页中创建一个超链接,但它没有显示,也没有给出任何错误。这是我的index.html.erb代码。ListingarticlesTitleTextssss我检查了我的路线,我认为它们也没有问题。PrefixVerbURIPatternController#Actionwelcome_indexGET/welcome/index(.:format)welcome#indexarticlesGET/articles(.:format)articles#indexPOST/articles(.:format)articles#createnew_articleGET/article

  8. ruby - 有人可以帮助解释类创建的 post_initialize 回调吗 (Sandi Metz) - 2

    我正在阅读SandiMetz的POODR,并且遇到了一个我不太了解的编码原则。这是代码:classBicycleattr_reader:size,:chain,:tire_sizedefinitialize(args={})@size=args[:size]||1@chain=args[:chain]||2@tire_size=args[:tire_size]||3post_initialize(args)endendclassMountainBike此代码将为其各自的属性输出1,2,3,4,5。我不明白的是查找方法。当一辆山地自行车被实例化时,因为它没有自己的initialize方法

  9. ruby-on-rails - RSpec:避免使用允许接收的任何实例 - 2

    我正在处理旧代码的一部分。beforedoallow_any_instance_of(SportRateManager).toreceive(:create).and_return(true)endRubocop错误如下:Avoidstubbingusing'allow_any_instance_of'我读到了RuboCop::RSpec:AnyInstance我试着像下面那样改变它。由此beforedoallow_any_instance_of(SportRateManager).toreceive(:create).and_return(true)end对此:let(:sport_

  10. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

随机推荐