`
RednaxelaFX
  • 浏览: 3013653 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

HotSpot 17.0-b12的逃逸分析/标量替换的一个演示

阅读更多
之前做了一次关于Java程序的执行的分享,里面提到了“逃逸分析”,以及它可以引出的一些优化,包括“标量替换”“栈上分配”“锁削除”之类。这里用一个实际例子来演示Sun的JDK 6中实现的标量替换优化。

简单说明两个概念:

逃逸分析,escape analysis。分析程序中指针的动态作用域,看某个指针是否指向某个固定的对象并且没有“逃逸”出某个函数/方法或者线程的范围。如果没有逃逸则可知该指针只在某个局部范围内可见,外部(别的函数/方法或线程)无法看到它。于是可以为别的一些优化提供保证。

标量替换,scalar replacement。Java中的原始类型无法再分解,可以看作标量(scalar);指向对象的引用也是标量;而对象本身则是聚合量(aggregate),可以包含任意个数的标量。如果把一个Java对象拆散,将其成员变量恢复为分散的变量,这就叫做标量替换。拆散后的变量便可以被单独分析与优化,可以各自分别在活动记录(栈帧或寄存器)上分配空间;原本的对象就无需整体分配空间了。

虽然概念上的JVM总是在Java堆上为对象分配空间,但并不是说完全依照概念的描述去实现;只要最后实现处理的“可见效果”与概念中描述的一直就没问题了。所以说,“you can cheat as long as you don't get caught”。Java对象在实际的JVM实现中可能在GC堆上分配空间,也可能在栈上分配空间,也可能完全就消失了。这种行为从Java源码中看不出来,也无法显式指定,只是聪明的JVM自动做的优化而已。

引用一段注释:
escape.cpp
// in order for an object to be scalar-replaceable, it must be:
//   - a direct allocation (not a call returning an object)
//   - non-escaping
//   - eligible to be a unique type
//   - not determined to be ineligible by escape analysis


普通Java对象的大小貌似不影响标量替换的判定,不过数组的大小则会影响。
escape.cpp, ConnectionGraph::process_call_result()
case Op_Allocate:
{
  Node *k = call->in(AllocateNode::KlassNode);
  const TypeKlassPtr *kt = k->bottom_type()->isa_klassptr();
  assert(kt != NULL, "TypeKlassPtr  required.");
  ciKlass* cik = kt->klass();

  PointsToNode::EscapeState es;
  uint edge_to;
  if (cik->is_subclass_of(_compile->env()->Thread_klass()) ||
     !cik->is_instance_klass() || // StressReflectiveCode
      cik->as_instance_klass()->has_finalizer()) {
    es = PointsToNode::GlobalEscape;
    edge_to = _phantom_object; // Could not be worse
  } else {
    es = PointsToNode::NoEscape;
    edge_to = call_idx;
  }
  set_escape_state(call_idx, es);
  add_pointsto_edge(resproj_idx, edge_to);
  _processed.set(resproj_idx);
  break;
}

case Op_AllocateArray:
{

  Node *k = call->in(AllocateNode::KlassNode);
  const TypeKlassPtr *kt = k->bottom_type()->isa_klassptr();
  assert(kt != NULL, "TypeKlassPtr  required.");
  ciKlass* cik = kt->klass();

  PointsToNode::EscapeState es;
  uint edge_to;
  if (!cik->is_array_klass()) { // StressReflectiveCode
    es = PointsToNode::GlobalEscape;
    edge_to = _phantom_object;
  } else {
    es = PointsToNode::NoEscape;
    edge_to = call_idx;
    int length = call->in(AllocateNode::ALength)->find_int_con(-1);
    if (length < 0 || length > EliminateAllocationArraySizeLimit) {
      // Not scalar replaceable if the length is not constant or too big.
      ptnode_adr(call_idx)->_scalar_replaceable = false;
    }
  }
  set_escape_state(call_idx, es);
  add_pointsto_edge(resproj_idx, edge_to);
  _processed.set(resproj_idx);
  break;
}

这里可以看到数组的大小(元素个数)与EliminateAllocationArraySizeLimit的关系。EliminateAllocationArraySizeLimit的默认值是64:
c2_globals.hpp
product(intx, EliminateAllocationArraySizeLimit, 64, "Array size (number of elements) limit for scalar replacement")


=================================================================

接下来开始实验。

从官网下载JDK 6 update 21 build 03的fastdebug版。运行java -server -version应可以看到:
command prompt 写道
java version "1.6.0_20-ea-fastdebug"
Java(TM) SE Runtime Environment (build 1.6.0_20-ea-fastdebug-b02)
Java HotSpot(TM) Server VM (build 17.0-b12-fastdebug, mixed mode)

本帖的例子都是使用上述JDK在32位Windows XP SP3上运行的。

该版本HotSpot的server模式在32位x86上的一些默认参数值如下:
CompileThreshold               = 10000
OnStackReplacePercentage       = 140
InterpreterProfilePercentage   = 33
InterpreterBackwardBranchLimit =
    (CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)) / 100
                               = 10700

注意到CompileThreshold与InterpreterBackwardBranchLimit这两个值:
CompileThreshold:当某个方法被调用+循环次数累计超过该值时,触发标准的JIT编译;
InterpreterBackwardBranchLimit:当某个方法调用+循环次数累计超过该值时,触发OSR形式的JIT编译。

下面就来造一个能演示逃逸分析/标量替换优化的例子。
我们希望能写一个test()方法,让它被调用足够次数以触发标准JIT编译;由于HotSpot默认采用后台编译,还得让程序运行足够时间让编译完成;最好能避免其它方法的编译,减少输出结果的干扰。
代码如下:
public class TestEAScalarReplacement {
    private static int fooValue;
    
    public static void test(int x) {
        int xx = x + 2;
        Point p = new Point(xx, 42);
        fooValue = p.getX();         // "publish" result
    }
    
    private static void driver0(int x) {
        for (int i = 0; i < 5; i++) {
            test(i + x);
        }
    }
    
    private static void driver1(int x) {
        for (int i = 0; i < 5; i++) {
            test(i + x);
        }
    }
    
    private static void driver2(int x) {
        for (int i = 0; i < 5; i++) {
            test(i + x);
        }
    }
    
    private static void driver3(int x) {
        for (int i = 0; i < 5; i++) {
            test(i + x);
        }
    }
    
    public static void driver(int x) {
        for (int i = 0; i < 40; i++) {
            driver0(i + x);
            driver1(i + x);
            driver2(i + x);
            driver3(i + x);
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < 40; i++) {
            driver(i);
        }
    }
}

class Point {
    private int x;
    private int y;
    
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() {
        return x;
    }
    
    public int getY() {
        return y;
    }
}

这里,Point是用于演示被替换为标量的类型,TestEAScalarReplacement.test()是目标方法,其它一些方法都是用于预热用的。

这种预热方式可以保证test()被调用足够次数,在收集足够多的profile信息的前提下触发标准JIT编译;而驱动预热的方法各自被调用的次数与循环的次数都不足以触发其OSR形式的JIT编译。

test()方法中,最后一句将一个p.x赋值给一个镜头变量。这是为了保证局部变量xx对应的计算能存活而写的。
由于成员变量与静态变量都“外部可见”,例如说从别的线程可以观察到这些值的变化,所以JVM不能够对成员变量与静态变量的赋值做太多优化。这里的赋值保证可以生效,于是p.x就被“使用”过,间接也就“使用”过局部变量xx的值。可以确定没有被使用过的值/计算则可以被优化掉,在最终生成的代码里就看不到了。
这种向成员变量或静态变量赋值的操作也叫做“发布”(publish)一个值。

OK,那么用下述参数跑一下该实验:
java -server -XX:+DoEscapeAnalysis -XX:+EliminateAllocations TestEAScalarReplacement


什么也没看到?呵呵,那就让HotSpot输出点日志来看看。下面是几个输出日志的参数与对应的日志:

-XX:+PrintCompilation
  3       TestEAScalarReplacement::test (23 bytes)

这里说明TestEAScalarReplacement.test()方法确实被编译了,而且是标准JIT编译。

-XX:+PrintInlining
      @ 11   Point::<init>  inline (hot)
        @ 1   java.lang.Object::<init>  inline (hot)
      @ 16   Point::getX  inline (hot)

这里说明test()方法编译时将Point与Object的构造器,以及Point.getX()方法内联了进去。也就是说,test()方法在内联后等价变为:
public static void test(int x) {
    int xx = x + 2;
    Point p = new_Point(); // 创建一个Point对象但不调用其构造器
    p.x = xx;              // Point构造器内容被内联到这里
    p.y = 42;
    fooValue = p.x;        // getX()方法也被内联了
}

留意到现在变量p指向的Point对象还是一个整体。

-XX:+PrintEscapeAnalysis
======== Connection graph for  TestEAScalarReplacement::test
    27 JavaObject NoEscape  [[ 94F 88F]]   27	Allocate	===  5  6  7  8  1 ( 25  23  24  1  1  22  1 ) [[ 28  29  30  37  38  39 ]]  rawptr:NotNull ( int:>=0, java/lang/Object:NotNull *, bool, int ) TestEAScalarReplacement::test @ bci:4 !jvms: TestEAScalarReplacement::test @ bci:4
LocalVar [[ 27P]]   39	Proj	===  27  [[ 40  94  88 ]] #5 !jvms: TestEAScalarReplacement::test @ bci:4

这里说明通过逃逸分析,发现变量p没有逃逸出test()方法。

-XX:+PrintEliminateAllocations
Scalar  27	Allocate	===  5  6  7  8  1 ( 25  23  24  1  1  22  1 ) [[ 28  29  30  37  38  39 ]]  rawptr:NotNull ( int:>=0, java/lang/Object:NotNull *, bool, int ) TestEAScalarReplacement::test @ bci:4 !jvms: TestEAScalarReplacement::test @ bci:4
++++ Eliminated: 27 Allocate

这里说明变量p指向的Point对象被标量替换了。被标量替换后的test()方法等价于:
public static void test(int x) {
    int xx = x + 2;
    int px = xx;    // Point消失了,留下其成员为分散的局部变量
    int py = 42;
    fooValue = px;
}


-XX:+PrintAssembly
Decoding compiled method 0x00be5248:
Code:
[Disassembling for mach='i386']
[Entry Point]
[Verified Entry Point]
  # {method} 'test' '(I)V' in 'TestEAScalarReplacement'
  # parm0:    ecx       = int
  #           [sp+0x10]  (sp of caller)
  0x00be5320: push   %ebp
  0x00be5321: sub    $0x8,%esp          ;*synchronization entry
                                        ; - TestEAScalarReplacement::test@-1 (line 5)
  0x00be5327: add    $0x2,%ecx
  0x00be532a: mov    $0x150,%ebp
  0x00be532f: mov    %ecx,0x1024b4e0(%ebp)  ;*putstatic fooValue
                                        ; - TestEAScalarReplacement::test@19 (line 7)
                                        ;   {oop('TestEAScalarReplacement')}
  0x00be5335: add    $0x8,%esp
  0x00be5338: pop    %ebp
  0x00be5339: test   %eax,0x9c0000      ;   {poll_return}
[Deopt MH Handler Code]
  0x00be533f: ret    
[Exception Handler]
[Stub Code]
  0x00be5340: jmp    0x00be4d40         ;   {no_reloc}
[Deopt Handler Code]
  0x00be5345: push   $0xbe5345          ;   {section_word}
  0x00be534a: jmp    0x00bcbb40         ;   {runtime_call}
[Constants]
  0x00be534f: int3

这里显示了test()方法最终被JIT编译器变成了怎样的x86指令(AT&T记法)。无法使用该参数的同学可以用-XX:+PrintOptoAssembly来代替,看到的是C2在最终生成代码之前的中间代码,已经非常接近最后结果了。

刨去一些结构信息,test()方法对应的编译结果是:
; 方法入口处理
0x00be5320: push   %ebp
0x00be5321: sub    $0x8,%esp

;; xx = x + 2
; (参数x从ECX寄存器传入,然后局部变量xx被叠加分配在ECX上)
0x00be5327: add    $0x2,%ecx

;; TestEAScalarReplacement.fooValue = xx
0x00be532a: mov    $0x150,%ebp
0x00be532f: mov    %ecx,0x1024b4e0(%ebp)

;; 方法出口处理
0x00be5335: add    $0x8,%esp
0x00be5338: pop    %ebp
; 下面这句是检查是否要进入safepoint
0x00be5339: test   %eax,0x9c0000      ;   {poll_return}
; 最后返回
0x00be533f: ret


也就是说,最后编译出来的test()方法等价于:
public static void test(int x) {
    fooValue = x + 2;
}


=================================================================

上面的实验演示了Sun的JDK 6中实现的逃逸分析与标量替换。不过话说回来,这个实现确实不够理想,要达到上面实验中这么明显的效果比较困难,特别是分析的代码规模一大就不太能行了……也就是说很RP。所以逃逸分析及相关优化即便在JDK 6的server模式默认也是不开的。
之前在JDK6u18的时候这些优化还被暂时禁用了一段时间。所以用6u18来测逃逸分析的同学们请换新版本来测吧,那个版本上看不到效果的……
分享到:
评论
16 楼 RednaxelaFX 2012-02-07  
haoweishow 写道
我的理解是CompileThreshold是触发方法进行JIT编译的阀值,当方法的调用次数超过该阀值,就会进行JIT编译,不知道这样理解是否正确?

不完全正确。正确的版本在这帖里已经写了。当然这个信不信由你了

haoweishow 写道
这个PPT的210页,提到了:方法调用计数器和回边计数器。你提到的循环次数 应该是回边计数吧?

没错。

haoweishow 写道
最近在看的书就是 林昊的《分布式java应用:基础与实践》,今天看到这个帖子,还专门翻了书,CompileThreshold的简单说明中 确实没有提到循环次数。

那是因为当时作者还没完全弄清楚这个点。这个我可以肯定嗯,因为后来有聊过。不过这本书出新版的时候你就不会看到这个错误了——可能是一两年后的事。

haoweishow 写道
不过在openjdk的源码中,看到跟CompileThreshold相关的地方基本都有提到了OSR。但是在深入就不明白了。。。

不是的。正常编译不涉及OSR。OSR编译的方法入口不在正常方法入口。两条路线是分开的。我的PPT里引用的那段代码就是实际上设定阈值的代码,跟这帖引用的是同一块。
15 楼 haoweishow 2012-02-07  
RednaxelaFX 写道
haoweishow 写道
引用
注意到CompileThreshold与InterpreterBackwardBranchLimit这两个值:
CompileThreshold:当某个方法被调用+循环次数累计超过该值时,触发标准的JIT编译;
InterpreterBackwardBranchLimit:当某个方法调用+循环次数累计超过该值时,触发OSR形式的JIT编译。


请问一下,我看到的资料CompileThreshold的解释都是某个方法被调用次数的阀值,你这里的循环次数是什么意思呢?

你看到的资料对CompileThreshold的描述是错的,或者是你的理解有偏差。请问是哪些资料有这样的描述?
请参考我的一个PPT,那边有流程图啥的,可能更容易理解些。205页开始:http://www.tektalk.org/2011/07/28/java-%E7%A8%8B%E5%BA%8F%E7%9A%84%E7%BC%96%E8%AF%91%EF%BC%8C%E5%8A%A0%E8%BD%BD-%E5%92%8C-%E6%89%A7%E8%A1%8C/

我的理解是CompileThreshold是触发方法进行JIT编译的阀值,当方法的调用次数超过该阀值,就会进行JIT编译,不知道这样理解是否正确?

这个PPT的210页,提到了:方法调用计数器和回边计数器。你提到的循环次数 应该是回边计数吧?

最近在看的书就是 林昊的《分布式java应用:基础与实践》,今天看到这个帖子,还专门翻了书,CompileThreshold的简单说明中 确实没有提到循环次数。

不过在openjdk的源码中,看到跟CompileThreshold相关的地方基本都有提到了OSR。但是在深入就不明白了。。。
14 楼 RednaxelaFX 2012-02-07  
haoweishow 写道
引用
注意到CompileThreshold与InterpreterBackwardBranchLimit这两个值:
CompileThreshold:当某个方法被调用+循环次数累计超过该值时,触发标准的JIT编译;
InterpreterBackwardBranchLimit:当某个方法调用+循环次数累计超过该值时,触发OSR形式的JIT编译。


请问一下,我看到的资料CompileThreshold的解释都是某个方法被调用次数的阀值,你这里的循环次数是什么意思呢?

你看到的资料对CompileThreshold的描述是错的,或者是你的理解有偏差。请问是哪些资料有这样的描述?
请参考我的一个PPT,那边有流程图啥的,可能更容易理解些。205页开始:http://www.tektalk.org/2011/07/28/java-%E7%A8%8B%E5%BA%8F%E7%9A%84%E7%BC%96%E8%AF%91%EF%BC%8C%E5%8A%A0%E8%BD%BD-%E5%92%8C-%E6%89%A7%E8%A1%8C/
13 楼 haoweishow 2012-02-07  
引用
注意到CompileThreshold与InterpreterBackwardBranchLimit这两个值:
CompileThreshold:当某个方法被调用+循环次数累计超过该值时,触发标准的JIT编译;
InterpreterBackwardBranchLimit:当某个方法调用+循环次数累计超过该值时,触发OSR形式的JIT编译。


请问一下,我看到的资料CompileThreshold的解释都是某个方法被调用次数的阀值,你这里的循环次数是什么意思呢?
12 楼 RednaxelaFX 2011-06-08  
考古:以前提到的标量替换的去优化(deoptimize)的事情,在hotspot-compiler-dev里有说过
Vladimir Kozlov 写道
From Vladimir.Kozlov at Sun.COM  Fri Mar  7 11:10:21 2008
From: Vladimir.Kozlov at Sun.COM (Vladimir Kozlov)
Date: Fri, 07 Mar 2008 11:10:21 -0800
Subject: Request for review (L): 6671807: Escape Analysis: Add new ideal node
to represent the state of a scalarized object at a safepoint
Message-ID: <47D1931D.7080605@sun.com>

http://webrev.invokedynamic.info/kvn/6671807/index.html

Fixed 6671807: Escape Analysis: Add new ideal node to represent the state of a scalarized object at a safepoint

Problem:
To reallocate a scalarized object during a deoptimization we need
to know the state (values) of object's non-static fields at a safepoint.

Solution:
Add new ideal node SafePointScalarObjectNode to represent the state of
a scalarized object at a safepoint. It describes additional input edges
(starting and count) in the safepoint node to which it is attached.
SafePointScalarObjectNode nodes will be created for each safepoint nodes
which reference the original scalar replaced allocation (this code
will be added in next changes).

Thanks,
Vladimir
11 楼 lucane 2011-05-15  
Thank you!
10 楼 RednaxelaFX 2011-05-15  
lucane 写道
另外我用PrintAssembly貌似没有看到任何conditionalBranch方法相关的信息,估计是我的方法不对(我就是把屏幕输出的重定向到文件),你是怎么看到<script src="https://gist.github.com/972929.js?file=PrintAssembly.log"></script>的呢?

PrintAssembly是输出到stdout的,我也是把stdout重定向到文件来跑测试的。
关于这个参数,正好请参考最近的一帖,JVM 反汇编动态运行代码。如果你在输出里看到“PrintAssembly is disabled”,那你多半就是缺少hsdis插件了。这个插件装好了就可以自己做实验来观察了。
如果你使用debug build的HotSpot Server VM,还可以使用-XX:+PrintOptoAssembly来看带有更多注解的、server编译器生成的中间代码。
另外,前面提到过有技巧来控制编译器生成代码更符合我们的特定目的。这个例子的编译器参数写在.hotspot_compiler文件里。注意不要漏了,内容也要根据你的测试代码的名字变更而相应调整。

lucane 写道
conditionalBranch这里的if else能不能形成你说的那种互斥关系?并且接收参数是1的情况远远要多于其他

嘿嘿,这样的话确实是互斥的。但现在的HotSpot server编译器仍然不会将它转换为你想像的那种形式。
Decoding compiled method 0x00007fb06d79e610:
Code:
[Disassembling for mach='i386:x86-64']
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} 'conditionalBranch' '(I)V' in 'TestC2BranchPrediction2'
  # parm0:    rsi       = int
  #           [sp+0x20]  (sp of caller)
  0x00007fb06d79e780: mov    %eax,-0x6000(%rsp)
  0x00007fb06d79e787: push   %rbp
  0x00007fb06d79e788: sub    $0x10,%rsp         ;*synchronization entry
                                                ; - TestC2BranchPrediction2::conditionalBranch@-1 (line 33)
  0x00007fb06d79e78c: test   %esi,%esi
  0x00007fb06d79e78e: je     0x00007fb06d79e7c6  ;*if_icmpne
                                                ; - TestC2BranchPrediction2::conditionalBranch@2 (line 33)
  0x00007fb06d79e790: cmp    $0x1,%esi
  0x00007fb06d79e793: jne    0x00007fb06d79e7b0  ;*if_icmpne
                                                ; - TestC2BranchPrediction2::conditionalBranch@15 (line 35)
  0x00007fb06d79e795: mov    $0x780080d80,%rsi  ;   {oop("DO SOMETHING B")}
  0x00007fb06d79e79f: callq  0x00007fb06d774c20  ; OopMap{off=36}
                                                ;*invokestatic marker
                                                ; - TestC2BranchPrediction2::conditionalBranch@20 (line 36)
                                                ;   {static_call}
  0x00007fb06d79e7a4: add    $0x10,%rsp
  0x00007fb06d79e7a8: pop    %rbp
  0x00007fb06d79e7a9: test   %eax,0x5683851(%rip)        # 0x00007fb072e22000
                                                ;   {poll_return}
  0x00007fb06d79e7af: retq   
  0x00007fb06d79e7b0: cmp    $0x2,%esi
  0x00007fb06d79e7b3: jne    0x00007fb06d79e7a4  ;*if_icmpne
                                                ; - TestC2BranchPrediction2::conditionalBranch@28 (line 37)
  0x00007fb06d79e7b5: mov    $0x780080dd0,%rsi  ;   {oop("DO SOMETHING C")}
  0x00007fb06d79e7bf: callq  0x00007fb06d774c20  ; OopMap{off=68}
                                                ;*invokestatic marker
                                                ; - TestC2BranchPrediction2::conditionalBranch@33 (line 38)
                                                ;   {static_call}
  0x00007fb06d79e7c4: jmp    0x00007fb06d79e7a4
  0x00007fb06d79e7c6: mov    $0x780080d30,%rsi  ;   {oop("DO SOMETHING A")}
  0x00007fb06d79e7d0: xchg   %ax,%ax
  0x00007fb06d79e7d3: callq  0x00007fb06d774c20  ; OopMap{off=88}
                                                ;*invokestatic marker
                                                ; - TestC2BranchPrediction2::conditionalBranch@7 (line 34)
                                                ;   {static_call}
  0x00007fb06d79e7d8: jmp    0x00007fb06d79e7a4  ;*invokestatic marker
                                                ; - TestC2BranchPrediction2::conditionalBranch@33 (line 38)
  0x00007fb06d79e7da: mov    %rax,%rsi
  0x00007fb06d79e7dd: jmp    0x00007fb06d79e7e7  ;*invokestatic marker
                                                ; - TestC2BranchPrediction2::conditionalBranch@7 (line 34)
  0x00007fb06d79e7df: mov    %rax,%rsi
  0x00007fb06d79e7e2: jmp    0x00007fb06d79e7e7
  0x00007fb06d79e7e4: mov    %rax,%rsi          ;*invokestatic marker
                                                ; - TestC2BranchPrediction2::conditionalBranch@20 (line 36)
  0x00007fb06d79e7e7: add    $0x10,%rsp
  0x00007fb06d79e7eb: pop    %rbp
  0x00007fb06d79e7ec: jmpq   0x00007fb06d799a60  ;   {runtime_call}
  0x00007fb06d79e7f1: hlt    
  0x00007fb06d79e7f2: hlt    
  0x00007fb06d79e7f3: hlt    
  0x00007fb06d79e7f4: hlt    
  0x00007fb06d79e7f5: hlt    
  0x00007fb06d79e7f6: hlt    
  0x00007fb06d79e7f7: hlt    
  0x00007fb06d79e7f8: hlt    
  0x00007fb06d79e7f9: hlt    
  0x00007fb06d79e7fa: hlt    
  0x00007fb06d79e7fb: hlt    
  0x00007fb06d79e7fc: hlt    
  0x00007fb06d79e7fd: hlt    
  0x00007fb06d79e7fe: hlt    
  0x00007fb06d79e7ff: hlt    
[Stub Code]
  0x00007fb06d79e800: mov    $0x0,%rbx          ;   {no_reloc}
  0x00007fb06d79e80a: jmpq   0x00007fb06d79e80a  ;   {runtime_call}
  0x00007fb06d79e80f: mov    $0x0,%rbx          ;   {static_stub}
  0x00007fb06d79e819: jmpq   0x00007fb06d79e819  ;   {runtime_call}
  0x00007fb06d79e81e: mov    $0x0,%rbx          ;   {static_stub}
  0x00007fb06d79e828: jmpq   0x00007fb06d79e828  ;   {runtime_call}
[Exception Handler]
  0x00007fb06d79e82d: jmpq   0x00007fb06d79a260  ;   {runtime_call}
[Deopt Handler Code]
  0x00007fb06d79e832: callq  0x00007fb06d79e837
  0x00007fb06d79e837: subq   $0x5,(%rsp)
  0x00007fb06d79e83c: jmpq   0x00007fb06d7757c0  ;   {runtime_call}
  0x00007fb06d79e841: std    
  0x00007fb06d79e842: incl   (%rax)
  0x00007fb06d79e844: add    %al,(%rax)
  0x00007fb06d79e846: add    %al,(%rax)

这段代码编译生成出来的结构跟先前的例子是一样的。仍然是先判断了是否为0再判断是否为1。

但是,如果这段代码的if...else if...改为switch的话,情况就会改变:
import java.util.Random;

public class TestC2BranchPrediction3 {
  public static void main(String[] args) {
    Random rand = new Random();

    int count0 = 0;
    int count2 = 0;

    for (int i = 0; i < 10000000L; i++) {
      int state = rand.nextInt(10);
      switch (state) {
      case 0:
        if(20 >= count0) {
            conditionalBranch(state);
            count0++;
        }
        break;
      case 1:
        conditionalBranch(state);
        break;
      case 2:
        if(200 >= count2) {
            conditionalBranch(state);
            count2++;
        }
        break;
      }
    }
  }

  public static void conditionalBranch(int flag) {
    switch (flag) {
    case 0:
      marker("DO SOMETHING A");
      break;
    case 1:
      marker("DO SOMETHING B");
      break;
    case 2:
      marker("DO SOMETHING C");
      break;
    }
  }

  public static void marker(String message) {
    try { message.intern(); } catch (Exception e) { }
  }
}

就会看到编译出来的代码结构变了:
Decoding compiled method 0x00007fe3087bc7d0:
Code:
[Disassembling for mach='i386:x86-64']
[Entry Point]
[Verified Entry Point]
[Constants]
  # {method} 'conditionalBranch' '(I)V' in 'TestC2BranchPrediction3'
  # parm0:    rsi       = int
  #           [sp+0x20]  (sp of caller)
  0x00007fe3087bc940: mov    %eax,-0x6000(%rsp)
  0x00007fe3087bc947: push   %rbp
  0x00007fe3087bc948: sub    $0x10,%rsp         ;*synchronization entry
                                                ; - TestC2BranchPrediction3::conditionalBranch@-1 (line 33)
  0x00007fe3087bc94c: cmp    $0x1,%esi
  0x00007fe3087bc94f: je     0x00007fe3087bc986
  0x00007fe3087bc951: cmp    $0x1,%esi
  0x00007fe3087bc954: jg     0x00007fe3087bc96e
  0x00007fe3087bc956: test   %esi,%esi
  0x00007fe3087bc958: jne    0x00007fe3087bc998  ;*tableswitch
                                                ; - TestC2BranchPrediction3::conditionalBranch@1 (line 33)
  0x00007fe3087bc95a: mov    $0x780080e10,%rsi  ;   {oop("DO SOMETHING A")}
  0x00007fe3087bc964: xchg   %ax,%ax
  0x00007fe3087bc967: callq  0x00007fe308793c20  ; OopMap{off=44}
                                                ;*invokestatic marker
                                                ; - TestC2BranchPrediction3::conditionalBranch@30 (line 35)
                                                ;   {static_call}
  0x00007fe3087bc96c: jmp    0x00007fe3087bc998
  0x00007fe3087bc96e: cmp    $0x3,%esi
  0x00007fe3087bc971: jge    0x00007fe3087bc998  ;*tableswitch
                                                ; - TestC2BranchPrediction3::conditionalBranch@1 (line 33)
  0x00007fe3087bc973: mov    $0x780080dc0,%rsi  ;   {oop("DO SOMETHING C")}
  0x00007fe3087bc97d: xchg   %ax,%ax
  0x00007fe3087bc97f: callq  0x00007fe308793c20  ; OopMap{off=68}
                                                ;*invokestatic marker
                                                ; - TestC2BranchPrediction3::conditionalBranch@46 (line 41)
                                                ;   {static_call}
  0x00007fe3087bc984: jmp    0x00007fe3087bc998
  0x00007fe3087bc986: mov    $0x780080d70,%rsi  ;   {oop("DO SOMETHING B")}
  0x00007fe3087bc990: xchg   %ax,%ax
  0x00007fe3087bc993: callq  0x00007fe308793c20  ; OopMap{off=88}
                                                ;*invokestatic marker
                                                ; - TestC2BranchPrediction3::conditionalBranch@38 (line 38)
                                                ;   {static_call}
  0x00007fe3087bc998: add    $0x10,%rsp
  0x00007fe3087bc99c: pop    %rbp
  0x00007fe3087bc99d: test   %eax,0x568465d(%rip)        # 0x00007fe30de41000
                                                ;   {poll_return}
  0x00007fe3087bc9a3: retq                      ;*invokestatic marker
                                                ; - TestC2BranchPrediction3::conditionalBranch@46 (line 41)
  0x00007fe3087bc9a4: mov    %rax,%rsi
  0x00007fe3087bc9a7: jmp    0x00007fe3087bc9b1  ;*invokestatic marker
                                                ; - TestC2BranchPrediction3::conditionalBranch@30 (line 35)
  0x00007fe3087bc9a9: mov    %rax,%rsi
  0x00007fe3087bc9ac: jmp    0x00007fe3087bc9b1
  0x00007fe3087bc9ae: mov    %rax,%rsi          ;*invokestatic marker
                                                ; - TestC2BranchPrediction3::conditionalBranch@38 (line 38)
  0x00007fe3087bc9b1: add    $0x10,%rsp
  0x00007fe3087bc9b5: pop    %rbp
  0x00007fe3087bc9b6: jmpq   0x00007fe3087b8a60  ;   {runtime_call}
  0x00007fe3087bc9bb: hlt    
  0x00007fe3087bc9bc: hlt    
  0x00007fe3087bc9bd: hlt    
  0x00007fe3087bc9be: hlt    
  0x00007fe3087bc9bf: hlt    
[Stub Code]
  0x00007fe3087bc9c0: mov    $0x0,%rbx          ;   {no_reloc}
  0x00007fe3087bc9ca: jmpq   0x00007fe3087bc9ca  ;   {runtime_call}
  0x00007fe3087bc9cf: mov    $0x0,%rbx          ;   {static_stub}
  0x00007fe3087bc9d9: jmpq   0x00007fe3087bc9d9  ;   {runtime_call}
  0x00007fe3087bc9de: mov    $0x0,%rbx          ;   {static_stub}
  0x00007fe3087bc9e8: jmpq   0x00007fe3087bc9e8  ;   {runtime_call}
[Exception Handler]
  0x00007fe3087bc9ed: jmpq   0x00007fe3087b9260  ;   {runtime_call}
[Deopt Handler Code]
  0x00007fe3087bc9f2: callq  0x00007fe3087bc9f7
  0x00007fe3087bc9f7: subq   $0x5,(%rsp)
  0x00007fe3087bc9fc: jmpq   0x00007fe3087947c0  ;   {runtime_call}
  0x00007fe3087bca01: add    %al,(%rax)
  0x00007fe3087bca03: add    %al,(%rax)
  0x00007fe3087bca05: add    %al,(%rax)
  0x00007fe3087bca07: .byte 0x0

这次就真的是把case 1放在最前面了。这是因为switch结构保证条件是互斥的,不用做额外的推导也能保证交换case的判断顺序不会出问题,实现起来比较容易所以HotSpot server编译器就给实现了。

lucane 写道
所以如果以Hotspot client方式执行会把条件(1 == flag)提前吗?

至于HotSpot Client VM里的client编译器,由于它不使用与每条字节码指令关联的profile数据,所以它不知道哪个分支比较热哪个比较冷,也就不会做相应的优化了。

lucane 写道
其实我就是一个疑惑,因为平常很多代码中都会存在好几个if else一长串的,一个参数接收的是什么值,执行对应的方法,JIT Compiler会不会走捷径,会不会出现条件重排情况,这样对于经常出现的情况就不会走过几次if else之后才到?
(因为有可能会出现风险,排错了估计也会出现很傻很慢的情况)

针对if...else if...这样形式的代码,我不肯定HotSpot server编译器是不是一定不会对条件做reordering,但条件判断不reorder而条件跳转的目标reorder的情况应该是比较常见的吧,因为这样做绝对安全。要非常精确的判断一串if...else if...里哪些地方实际上没有控制流依赖挺麻烦的。
以后我读代码的时候如果留意到专门处理这部分的代码再回复这个问题吧 ^_^
不知道wkoffee同学对此有没有经验呢?或者其它同学?
9 楼 lucane 2011-05-15  
谢谢你的回答,如果是这段代码呢?
import java.util.Random;

public class TestC2BranchPrediction2 {
  public static void main(String[] args) {
    Random rand = new Random();

    int count0 = 0;
    int count2 = 0;

    for (int i = 0; i < 10000000L; i++) {
      int state = rand.nextInt(10);
      switch (state) {
      case 0:
        if(20 >= count0) {
            conditionalBranch(state);
            count0++;
        }
        break;
      case 1:
        conditionalBranch(state);
        break;
      case 2:
        if(200 >= count2) {
            conditionalBranch(state);
            count2++;
        }
        break;
      }
    }
  }

  public static void conditionalBranch(int flag) {
    if (0 == flag) {
      marker("DO SOMETHING A");
    } else if (1 == flag) {
      marker("DO SOMETHING B");
    } else if (2 == flag) {
      marker("DO SOMETHING C");
    }
  }

  public static void marker(String message) {
    try { message.intern(); } catch (Exception e) { }
  }
}


conditionalBranch这里的if else能不能形成你说的那种互斥关系?并且接收参数是1的情况远远要多于其他

所以如果以Hotspot client方式执行会把条件(1 == flag)提前吗?
如果以Hotspot server方式呢?

另外我用PrintAssembly貌似没有看到任何conditionalBranch方法相关的信息,估计是我的方法不对(我就是把屏幕输出的重定向到文件),你是怎么看到<script src="https://gist.github.com/972929.js?file=PrintAssembly.log"></script>的呢?

其实我就是一个疑惑,因为平常很多代码中都会存在好几个if else一长串的,一个参数接收的是什么值,执行对应的方法,JIT Compiler会不会走捷径,会不会出现条件重排情况,这样对于经常出现的情况就不会走过几次if else之后才到?
(因为有可能会出现风险,排错了估计也会出现很傻很慢的情况)

可能我这问题不是很专业和清楚,希望你能理解我的意思,^_^

8 楼 RednaxelaFX 2011-05-15  
lucane 写道
请教个问题

比如这段代码

if(conditionA) {
    // DO SOMETHING A
} else if(conditionB) {
    // DO SOMETHING B
} else if(conditionC) {
    // DO SOMETHING C
}

执行的时候发现conditionB满足的次数要远远多于conditionA

代码被动态编译成机器码的时候它会不会考虑到把B这段代码提前

以前貌似看过这方面的东西,但忘记在哪看了

如果这样写的话,DO SOMETHING B对conditionA和conditionB都有控制流依赖。除非A和B有互斥关系不然改变条件顺序是不安全的。虽然条件判断的执行顺序不便于改变,但条件跳转的目标在代码里出现的顺序是可以改变的(把条件判断反转的话,原本在then分支的代码就会跟else分支的交换)。

HotSpot的server编译器能通过profile信息知道每个条件跳转的taken/not-taken的次数和比例状况,并相应的决定某个条件跳转要不要反转,目标是尽量让比较热的路径“拉直”,而将比较冷的路径放到一边去(out-of-line)。

根据你的问题,造了个例子:https://gist.github.com/972929
在Linux x64上的JDK 6 update 25跑的。用了些技巧让marker()方法不被conditionalBranch()方法内联,这样方便观察conditionalBranch()方法里的状况。
经过HotSpot server编译器的编译,得到的代码类似这样(伪代码):
public static void conditionalBranch(boolean flagA, boolean flagB, boolean flagC) {
  if (flagA)  goto isA;
  if (!flagB) goto notB;
  marker("DO SOMETHING B");
doReturn:
  return;

isA:
  marker("DO SOMETHING A");
  goto doReturn;

notB:
  if (flagC)
    marker("DO SOMETHING C");
  goto doReturn;
}

(省略掉末尾与异常处理相关的部分,那些代码本来也不会被执行)
这样可以看到,HotSpot server编译器确实可以把条件跳转的目标的顺序重新安排,让执行概率高的代码路径尽量形成一条直线。但条件判断本身的执行顺序还是维持跟原本一致的。也有条件判断本身也重排序的状况…不过不适用在这里的连串if...else if...
7 楼 lucane 2011-05-15  
请教个问题

比如这段代码

if(conditionA) {
    // DO SOMETHING A
} else if(conditionB) {
    // DO SOMETHING B
} else if(conditionC) {
    // DO SOMETHING C
}

执行的时候发现conditionB满足的次数要远远多于conditionA

代码被动态编译成机器码的时候它会不会考虑到把B这段代码提前

以前貌似看过这方面的东西,但忘记在哪看了
6 楼 RednaxelaFX 2010-05-07  
wkoffee 写道
谢谢你的耐心答复

我觉得和我原先的设想差不多,hotspot现在作栈分配还是有困难。要实现栈分配,escape analysis并不是难点,主要是对原先的其他部分改动是难点,尤其hotspot存在转回解释模式的deoptimize机制,即使对象不被其他线程可见,也要能在safepoint复原。

关于SafePointScalarObjectNode,我认为它是用来保存复原对象的信息,生成相应的oopMap或者debugInfo,但这个只是解决问题的第一步,在deopt中如何恢复,是在栈上复原还是在堆上恢复我觉得都有问题,等看了相应的代码后再来讨论

要支持deopt不只需要保持debug信息(BCI之类),还有很重要的是要保留“需要的计算”,换句话说要保证某些变量的存活。有些值可能只在uncommon路径上有被使用,而在被编译的hot path上没有使用过;这时就要看它们是否会影响deopt,如果会的话它们依赖的值是否在hot path上算出来了。所有会影响deopt的值都得保持存活。

标量替换后的对象要在堆上重新构造,需要:
1、准确的类型信息
2、所有成员域的值
所以当一个对象是NoEscape但无法判断其准确类型,例如说通过变量来反射创建对象时,_scalar_replaceable也会在逃逸分析中被设为false。
为了保证所有需要的成员域都能在deopt的时候存在,就要保持所有相关计算的存活。
基本思路就是这样了 >_<
5 楼 wkoffee 2010-05-07  
谢谢你的耐心答复

我觉得和我原先的设想差不多,hotspot现在作栈分配还是有困难。要实现栈分配,escape analysis并不是难点,主要是对原先的其他部分改动是难点,尤其hotspot存在转回解释模式的deoptimize机制,即使对象不被其他线程可见,也要能在safepoint复原。

关于SafePointScalarObjectNode,我认为它是用来保存复原对象的信息,生成相应的oopMap或者debugInfo,但这个只是解决问题的第一步,在deopt中如何恢复,是在栈上复原还是在堆上恢复我觉得都有问题,等看了相应的代码后再来讨论
4 楼 RednaxelaFX 2010-05-06  
wkoffee 写道
我比较好奇的一点是对象在栈上分配后能否在safepoint重组

一个对象如果满足标量替换的条件,就必然无法被别的线程见到。在safepoint也不例外。
对象在标量替换之后不需要在GC时重新扔到栈上;能减轻GC压力正是它的好处之一。

不过在deoptimize的时候,之前被乐观优化而标量替换的对象是需要重组并在GC堆上分配空间的。强制deoptimize时需要到达safepoint,这里可以跟safepoint扯上关系。
事实上在C2的标量替换实现中,safepoint处理占了一大块,Ideal中甚至有专门的节点来描述这种信息:SafePointScalarObjectNode
标量替换的主要实现在PhaseMacroExpand::scalar_replacement()。

目前C2貌似是不做“栈上分配”的优化的。在注释里可以看到:
// Note: currently there are no difference in compiler optimizations
// for ArgEscape objects and NoEscape objects which are not
// scalar replaceable.

对ArgEscape(参数逃逸:逃逸出了方法但没有逃逸出线程)与无法被标量替换的NoEscape(未逃逸:完全没有逃逸出方法),C2现在应该是只做了锁削除的优化,而没有实现栈上分配的优化。“理论上”通过逃逸分析是可以做栈上分配优化的,但“现实”是还没做 =_=|||

由于JIT有时间压力,C2用的逃逸分析算法是流不敏感的,于是有些情况就分析不出来了,也影响了能优化的东西的多少——但在JIT里用流敏感分析也是相当冒险的事,付出了开销后如果发现没多少能优化的东西那就杯具了 =_-||
3 楼 wkoffee 2010-05-06  
我比较好奇的一点是对象在栈上分配后能否在safepoint重组
2 楼 RednaxelaFX 2010-05-05  
lwwin 写道
只看了二只定义:

逃逸分析 ~ 作用域分析?
标量分析 ~ 聚合功能拆分?

可以这样理解吗?

第一个差不多,第二个……你说的是啥?
逃逸分析的关注点是动态作用域,也就是运行的时候某个指针“可见”的范围;讨论编程语言的时候,提到作用域经常是指词法作用域(lexical scope)或者静态作用域(static scope),这种在源码里就可以直接分析出来;而动态作用域则无法直观的从源码看出。

标量替换就是把聚合量拆掉,变成分散的标量,然后再去应用别的优化。上面的例子里,
class Point {
    private int x;
    private int y;
}

这是一个聚合量的类型

public static void test() {
    Point p = new Point(...);
    // ...
}

这是用一个局部变量指向了一个对象(聚合量)

public static void test() {
    int px;
    int py;
}

这是进行了标量替换后的状况,p不需要了,原本p指向的聚合量被拆成了px与py这俩标量。

这个概念应该……挺直观的吧?
1 楼 lwwin 2010-05-05  
只看了二只定义:

逃逸分析 ~ 作用域分析?
标量分析 ~ 聚合功能拆分?

可以这样理解吗?

相关推荐

Global site tag (gtag.js) - Google Analytics