- 浏览: 3037254 次
- 性别:
- 来自: 海外
-
文章分类
- 全部博客 (430)
- Programming Languages (23)
- Compiler (20)
- Virtual Machine (57)
- Garbage Collection (4)
- HotSpot VM (26)
- Mono (2)
- SSCLI Rotor (1)
- Harmony (0)
- DLR (19)
- Ruby (28)
- C# (38)
- F# (3)
- Haskell (0)
- Scheme (1)
- Regular Expression (5)
- Python (4)
- ECMAScript (2)
- JavaScript (18)
- ActionScript (7)
- Squirrel (2)
- C (6)
- C++ (10)
- D (2)
- .NET (13)
- Java (86)
- Scala (1)
- Groovy (3)
- Optimization (6)
- Data Structure and Algorithm (3)
- Books (4)
- WPF (1)
- Game Engines (7)
- 吉里吉里 (12)
- UML (1)
- Reverse Engineering (11)
- NSIS (4)
- Utilities (3)
- Design Patterns (1)
- Visual Studio (9)
- Windows 7 (3)
- x86 Assembler (1)
- Android (2)
- School Assignment / Test (6)
- Anti-virus (1)
- REST (1)
- Profiling (1)
- misc (39)
- NetOA (12)
- rant (6)
- anime (5)
- Links (12)
- CLR (7)
- GC (1)
- OpenJDK (2)
- JVM (4)
- KVM (0)
- Rhino (1)
- LINQ (2)
- JScript (0)
- Nashorn (0)
- Dalvik (1)
- DTrace (0)
- LLVM (0)
- MSIL (0)
最新评论
-
mldxs:
虽然很多还是看不懂,写的很好!
虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩 -
HanyuKing:
Java的多维数组 -
funnyone:
Java 8的default method与method resolution -
ljs_nogard:
Xamarin workbook - .Net Core 中不 ...
LINQ的恶搞…… -
txm119161336:
allocatestlye1 顺序为 // Fields o ...
最近做的两次Java/JVM分享的概要
之前做了一次关于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
普通Java对象的大小貌似不影响标量替换的判定,不过数组的大小则会影响。
escape.cpp, ConnectionGraph::process_call_result()
这里可以看到数组的大小(元素个数)与EliminateAllocationArraySizeLimit的关系。EliminateAllocationArraySizeLimit的默认值是64:
c2_globals.hpp
=================================================================
接下来开始实验。
从官网下载JDK 6 update 21 build 03的fastdebug版。运行java -server -version应可以看到:
本帖的例子都是使用上述JDK在32位Windows XP SP3上运行的。
该版本HotSpot的server模式在32位x86上的一些默认参数值如下:
注意到CompileThreshold与InterpreterBackwardBranchLimit这两个值:
CompileThreshold:当某个方法被调用+循环次数累计超过该值时,触发标准的JIT编译;
InterpreterBackwardBranchLimit:当某个方法调用+循环次数累计超过该值时,触发OSR形式的JIT编译。
下面就来造一个能演示逃逸分析/标量替换优化的例子。
我们希望能写一个test()方法,让它被调用足够次数以触发标准JIT编译;由于HotSpot默认采用后台编译,还得让程序运行足够时间让编译完成;最好能避免其它方法的编译,减少输出结果的干扰。
代码如下:
这里,Point是用于演示被替换为标量的类型,TestEAScalarReplacement.test()是目标方法,其它一些方法都是用于预热用的。
这种预热方式可以保证test()被调用足够次数,在收集足够多的profile信息的前提下触发标准JIT编译;而驱动预热的方法各自被调用的次数与循环的次数都不足以触发其OSR形式的JIT编译。
test()方法中,最后一句将一个p.x赋值给一个镜头变量。这是为了保证局部变量xx对应的计算能存活而写的。
由于成员变量与静态变量都“外部可见”,例如说从别的线程可以观察到这些值的变化,所以JVM不能够对成员变量与静态变量的赋值做太多优化。这里的赋值保证可以生效,于是p.x就被“使用”过,间接也就“使用”过局部变量xx的值。可以确定没有被使用过的值/计算则可以被优化掉,在最终生成的代码里就看不到了。
这种向成员变量或静态变量赋值的操作也叫做“发布”(publish)一个值。
OK,那么用下述参数跑一下该实验:
什么也没看到?呵呵,那就让HotSpot输出点日志来看看。下面是几个输出日志的参数与对应的日志:
-XX:+PrintCompilation
这里说明TestEAScalarReplacement.test()方法确实被编译了,而且是标准JIT编译。
-XX:+PrintInlining
这里说明test()方法编译时将Point与Object的构造器,以及Point.getX()方法内联了进去。也就是说,test()方法在内联后等价变为:
留意到现在变量p指向的Point对象还是一个整体。
-XX:+PrintEscapeAnalysis
这里说明通过逃逸分析,发现变量p没有逃逸出test()方法。
-XX:+PrintEliminateAllocations
这里说明变量p指向的Point对象被标量替换了。被标量替换后的test()方法等价于:
-XX:+PrintAssembly
这里显示了test()方法最终被JIT编译器变成了怎样的x86指令(AT&T记法)。无法使用该参数的同学可以用-XX:+PrintOptoAssembly来代替,看到的是C2在最终生成代码之前的中间代码,已经非常接近最后结果了。
刨去一些结构信息,test()方法对应的编译结果是:
也就是说,最后编译出来的test()方法等价于:
=================================================================
上面的实验演示了Sun的JDK 6中实现的逃逸分析与标量替换。不过话说回来,这个实现确实不够理想,要达到上面实验中这么明显的效果比较困难,特别是分析的代码规模一大就不太能行了……也就是说很RP。所以逃逸分析及相关优化即便在JDK 6的server模式默认也是不开的。
之前在JDK6u18的时候这些优化还被暂时禁用了一段时间。所以用6u18来测逃逸分析的同学们请换新版本来测吧,那个版本上看不到效果的……
不完全正确。正确的版本在这帖里已经写了。当然这个信不信由你了
没错。
那是因为当时作者还没完全弄清楚这个点。这个我可以肯定嗯,因为后来有聊过。不过这本书出新版的时候你就不会看到这个错误了——可能是一两年后的事。
不是的。正常编译不涉及OSR。OSR编译的方法入口不在正常方法入口。两条路线是分开的。我的PPT里引用的那段代码就是实际上设定阈值的代码,跟这帖引用的是同一块。
请问一下,我看到的资料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。但是在深入就不明白了。。。
请问一下,我看到的资料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的解释都是某个方法被调用次数的阀值,你这里的循环次数是什么意思呢?
PrintAssembly是输出到stdout的,我也是把stdout重定向到文件来跑测试的。
关于这个参数,正好请参考最近的一帖,JVM 反汇编动态运行代码。如果你在输出里看到“PrintAssembly is disabled”,那你多半就是缺少hsdis插件了。这个插件装好了就可以自己做实验来观察了。
如果你使用debug build的HotSpot Server VM,还可以使用-XX:+PrintOptoAssembly来看带有更多注解的、server编译器生成的中间代码。
另外,前面提到过有技巧来控制编译器生成代码更符合我们的特定目的。这个例子的编译器参数写在.hotspot_compiler文件里。注意不要漏了,内容也要根据你的测试代码的名字变更而相应调整。
嘿嘿,这样的话确实是互斥的。但现在的HotSpot server编译器仍然不会将它转换为你想像的那种形式。
这段代码编译生成出来的结构跟先前的例子是一样的。仍然是先判断了是否为0再判断是否为1。
但是,如果这段代码的if...else if...改为switch的话,情况就会改变:
就会看到编译出来的代码结构变了:
这次就真的是把case 1放在最前面了。这是因为switch结构保证条件是互斥的,不用做额外的推导也能保证交换case的判断顺序不会出问题,实现起来比较容易所以HotSpot server编译器就给实现了。
至于HotSpot Client VM里的client编译器,由于它不使用与每条字节码指令关联的profile数据,所以它不知道哪个分支比较热哪个比较冷,也就不会做相应的优化了。
针对if...else if...这样形式的代码,我不肯定HotSpot server编译器是不是一定不会对条件做reordering,但条件判断不reorder而条件跳转的目标reorder的情况应该是比较常见的吧,因为这样做绝对安全。要非常精确的判断一串if...else if...里哪些地方实际上没有控制流依赖挺麻烦的。
以后我读代码的时候如果留意到专门处理这部分的代码再回复这个问题吧 ^_^
不知道wkoffee同学对此有没有经验呢?或者其它同学?
如果这样写的话,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编译器的编译,得到的代码类似这样(伪代码):
(省略掉末尾与异常处理相关的部分,那些代码本来也不会被执行)
这样可以看到,HotSpot server编译器确实可以把条件跳转的目标的顺序重新安排,让执行概率高的代码路径尽量形成一条直线。但条件判断本身的执行顺序还是维持跟原本一致的。也有条件判断本身也重排序的状况…不过不适用在这里的连串if...else if...
要支持deopt不只需要保持debug信息(BCI之类),还有很重要的是要保留“需要的计算”,换句话说要保证某些变量的存活。有些值可能只在uncommon路径上有被使用,而在被编译的hot path上没有使用过;这时就要看它们是否会影响deopt,如果会的话它们依赖的值是否在hot path上算出来了。所有会影响deopt的值都得保持存活。
标量替换后的对象要在堆上重新构造,需要:
1、准确的类型信息
2、所有成员域的值
所以当一个对象是NoEscape但无法判断其准确类型,例如说通过变量来反射创建对象时,_scalar_replaceable也会在逃逸分析中被设为false。
为了保证所有需要的成员域都能在deopt的时候存在,就要保持所有相关计算的存活。
基本思路就是这样了 >_<
一个对象如果满足标量替换的条件,就必然无法被别的线程见到。在safepoint也不例外。
对象在标量替换之后不需要在GC时重新扔到栈上;能减轻GC压力正是它的好处之一。
不过在deoptimize的时候,之前被乐观优化而标量替换的对象是需要重组并在GC堆上分配空间的。强制deoptimize时需要到达safepoint,这里可以跟safepoint扯上关系。
事实上在C2的标量替换实现中,safepoint处理占了一大块,Ideal中甚至有专门的节点来描述这种信息:SafePointScalarObjectNode
标量替换的主要实现在PhaseMacroExpand::scalar_replacement()。
目前C2貌似是不做“栈上分配”的优化的。在注释里可以看到:
对ArgEscape(参数逃逸:逃逸出了方法但没有逃逸出线程)与无法被标量替换的NoEscape(未逃逸:完全没有逃逸出方法),C2现在应该是只做了锁削除的优化,而没有实现栈上分配的优化。“理论上”通过逃逸分析是可以做栈上分配优化的,但“现实”是还没做 =_=|||
由于JIT有时间压力,C2用的逃逸分析算法是流不敏感的,于是有些情况就分析不出来了,也影响了能优化的东西的多少——但在JIT里用流敏感分析也是相当冒险的事,付出了开销后如果发现没多少能优化的东西那就杯具了 =_-||
第一个差不多,第二个……你说的是啥?
逃逸分析的关注点是动态作用域,也就是运行的时候某个指针“可见”的范围;讨论编程语言的时候,提到作用域经常是指词法作用域(lexical scope)或者静态作用域(static scope),这种在源码里就可以直接分析出来;而动态作用域则无法直观的从源码看出。
标量替换就是把聚合量拆掉,变成分散的标量,然后再去应用别的优化。上面的例子里,
这是一个聚合量的类型
这是用一个局部变量指向了一个对象(聚合量)
这是进行了标量替换后的状况,p不需要了,原本p指向的聚合量被拆成了px与py这俩标量。
这个概念应该……挺直观的吧?
简单说明两个概念:
逃逸分析,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)
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编译,不知道这样理解是否正确?
不完全正确。正确的版本在这帖里已经写了。当然这个信不信由你了
![](/images/smiles/icon_lol.gif)
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:当某个方法被调用+循环次数累计超过该值时,触发标准的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:当某个方法被调用+循环次数累计超过该值时,触发标准的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:当某个方法被调用+循环次数累计超过该值时,触发标准的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
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!
![](/images/smiles/icon_idea.gif)
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
谢谢你的回答,如果是这段代码呢?
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之后才到?
(因为有可能会出现风险,排错了估计也会出现很傻很慢的情况)
可能我这问题不是很专业和清楚,希望你能理解我的意思,^_^
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这段代码提前
以前貌似看过这方面的东西,但忘记在哪看了
比如这段代码
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这段代码提前
以前貌似看过这方面的东西,但忘记在哪看了
比如这段代码
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中如何恢复,是在栈上复原还是在堆上恢复我觉得都有问题,等看了相应的代码后再来讨论
我觉得和我原先的设想差不多,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中如何恢复,是在栈上复原还是在堆上恢复我觉得都有问题,等看了相应的代码后再来讨论
我觉得和我原先的设想差不多,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
只看了二只定义:
逃逸分析 ~ 作用域分析?
标量分析 ~ 聚合功能拆分?
可以这样理解吗?
逃逸分析 ~ 作用域分析?
标量分析 ~ 聚合功能拆分?
可以这样理解吗?
发表评论
-
The Prehistory of Java, HotSpot and Train
2014-06-02 08:18 0http://cs.gmu.edu/cne/itcore/vi ... -
MSJVM and Sun 1.0.x/1.1.x
2014-05-20 18:50 0当年的survey paper: http://www.sym ... -
Sun JDK1.4.2_28有TieredCompilation
2014-05-12 08:48 0原来以前Sun的JDK 1.4.2 update 28就已经有 ... -
IBM JVM notes (2014 ver)
2014-05-11 07:16 0Sovereign JIT http://publib.bou ... -
class data sharing by Apple
2014-03-28 05:17 0class data sharing is implement ... -
HotSpot Server VM与Server Class Machine
2014-02-18 13:21 0HotSpot VM历来有Client VM与Server V ... -
Java 8的lambda表达式在OpenJDK8中的实现
2014-02-04 12:08 0三月份JDK8就要发布首发了,现在JDK8 release c ... -
GC stack map与deopt stack map的异同
2014-01-08 09:56 0两者之间不并存在包含关系。它们有交集,但也各自有特别的地方。 ... -
HotSpot Server Compiler与data-flow analysis
2014-01-07 17:41 0http://en.wikipedia.org/wiki/Da ... -
基于LLVM实现VM的JIT的一些痛点
2014-01-07 17:25 0同事Philip Reames Sanjoy Das http ... -
tailcall notes
2013-12-27 07:42 0http://blogs.msdn.com/b/clrcode ... -
《自制编程语言》的一些笔记
2013-11-24 00:20 0http://kmaebashi.com/programmer ... -
字符串的一般封装方式的内存布局 (1): 元数据与字符串内容,整体还是分离?
2013-11-07 17:44 22342(Disclaimer:未经许可请 ... -
字符串的一般封装方式的内存布局 (0): 拿在手上的是什么
2013-11-04 18:22 21455(Disclaimer:未经许可请 ... -
字符串的一般封装方式的内存布局
2013-11-01 12:55 0(Disclaimer:未经许可请 ... -
关于string,内存布局,C++ std::string,CoW
2013-10-30 20:45 0(Disclaimer:未经许可请 ... -
Java的instanceof是如何实现的
2013-09-22 16:57 0Java语言规范,Java SE 7版 http://docs ... -
也谈类型: 数据, 类型, 标签
2013-08-18 01:59 0numeric tower http://en.wikiped ... -
oop、klass、handle的关系
2013-07-30 17:34 0oopDesc及其子类的实例 oop : oopDesc* ... -
Nashorn各种笔记
2013-07-15 17:03 0http://bits.netbeans.org/netbea ...
相关推荐
OpenJDK17U-jdk-x64-windows-hotspot-17.0.5-8,vscode配置java环境需要
jdk1.8 jdk8java version "1.8.0_191"Java(TM) SE Runtime Environment (build 1.8.0_191-b12)Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
上传文件目录为:/home/ut/java/,cp当前目录下jdk-6u30-linux-i586.bin到/usr/lib/jdk/,chmod 777 jdk-6u30-linux-i586.bin jdk安装路径:/usr/lib/jdk/... Java HotSpot(TM) Server VM (build 20.5-b03, mixed mode)
Java是一种广泛使用的面向对象的编程语言,以其“一次编写,到处运行”的特性闻名。JVM(Java虚拟机)是Java程序执行的核心,它允许开发者在任何支持Java的平台上运行代码,而无需关心底层硬件的差异。这个压缩包...
因为包太大共两份:( jdk-6u30-linux-i586.part1.rar \( jdk-6u30-linux-i586.part2.rar ) 上传文件目录为:/home/ut/java/,cp当前目录下jdk-6u30-linux... Java HotSpot(TM) Server VM (build 20.5-b03, mixed mode)
OpenJDK17U-jdk_x64_windows_hotspot_17.0.4_8 官网下载太慢,传到这分享
AdoptOpenJDK使用基础结构,构建和测试脚本来从OpenJDK™类库以及OpenJDK HotSpot或Eclipse OpenJ9 VM中选择生成预构建的二进制文件。 所有AdoptOpenJDK二进制文件和脚本都是开源许可的,并且免费提供。
HotspotShield-10.11.4-hss-805-ext
Java HotSpot(TM) Client VM (build 20.13-b02, mixed mode, sharing) 4.安装hadoop (3台都安) [root@a1 ~]# tar zxvf hadoop-0.20.2-cdh3u5.tar.gz -C /usr/local 编辑hadoop 配置文件 [root@a1 ~]# cd /usr/...
OpenJDK17U-jdk_x64_windows_hotspot_17.0.1_12.msi
openjdk 17
有2个候选项可用于替换java(提供/usr/bin/java)。 选择 路径 优先级 状态 ------------------------------------------------------------ 0 /usr/lib/jvm/java-6-openjdk/jre/bin/java 1061 自动模式 1 /usr/...
- **`/ip address add`**: 另一个示例: ```plaintext /ip address add address=192.168.1.250/24 interface=ether2 ``` 在接口ether2上配置IP地址192.168.1.250/24。 - **`/ip route add`**: 用于配置默认...
深入解析hotspot实战!内容比较翔实、精辟,值得收藏!
然后,将解压后的JDK文件夹移动到`/usr/lib/jvm`目录下,如果没有这个目录,需要先通过命令`mkdir /usr/lib/jvm`创建它。这一步操作可以确保系统能够正确识别JDK的安装位置。 #### 知识点四:重命名JDK文件夹 为了...
标题中的"Linux环境OpenJDK8U-jdk-x64-linux-hotspot-8u372b07.tar.gz"指的是在Linux操作系统环境下使用的OpenJDK 8的更新版本(Update 372,Build 7)的HotSpot虚拟机实现。OpenJDK是一个开源的Java开发工具包,它...
HotSpot JVM 是一个多语言虚拟机,可以运行 Java、JavaScript、Scala、Ruby 等语言的程序,并支持 Windows、Mac OS X、Linux、Solaris、SPARC 和 PPC 等多种平台。HotSpot JVM 的编译过程可以分为几个阶段,包括 ...
在终端中,使用`mkdir`命令创建一个新的目录,例如 `/usr/lib/jvm`,用于存放JDK文件。 ```bash sudo mkdir /usr/lib/jvm ``` 2. **解压缩JDK**: 使用`tar`命令解压缩下载的`.tar.gz`文件到刚刚创建的目录。...
#### 一、前言 在Linux环境下,尤其是Ubuntu发行版中配置Java运行环境是非常常见的需求。本文将详细介绍如何在Ubuntu系统上安装JDK 1.8并配置其环境变量的过程。通过本教程的学习,您不仅能够掌握具体的步骤,还能...