首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > 编程 >

RE:循环话语的写法、Client和Server的性能差异以及microbenchmark的不准确

2012-11-01 
RE:循环语句的写法、Client和Server的性能差异以及microbenchmark的不准确这篇文章也是个回帖,继续用来偷懒

RE:循环语句的写法、Client和Server的性能差异以及microbenchmark的不准确
这篇文章也是个回帖,继续用来偷懒发blog
=====================================================

  一位朋友给了下面这段代码(在他给的代码中略作修改,避免了引入Random类、Integer装箱类导致不必要的因素),提出了2个问题:1.for (int i = 0, n = list.size(); i < n; i++)的写法是否会比for (int i = 0; i < list.size(); i++)更快?2.为何这段代码在Server VM下测出来的速度比Client VM还慢?

public class Client1 {public static void main(String[] args) {List<Object> list = new ArrayList<Object>();Object obj = new Object();// 填充数据for (int i = 0; i < 200000; i++) {list.add(obj);}long start;start = System.nanoTime();// 初始化时已经计算好条件for (int i = 0, n = list.size(); i < n; i++) {}System.out.println("判断条件中计算:" + (System.nanoTime() - start) + " ns");start = System.nanoTime();// 在判断条件中计算for (int i = 0; i < list.size(); i++) {}System.out.println("判断条件中计算:" + (System.nanoTime() - start) + " ns");}}

  首先来看看在代码最终执行时,for (int i = 0, n = list.size(); i < n; i++)的写法是否会比for (int i = 0; i < list.size(); i++)更快,用于测试的虚拟机是:
D:\_DevSpace\jdk1.7.0\fastdebug\bin>java -versionjava version "1.7.0-ea-fastdebug"Java(TM) SE Runtime Environment (build 1.7.0-ea-fastdebug-b127)Java HotSpot(TM) Client VM (build 20.0-b06-fastdebug, mixed mode)

  调试参数就加了一个-XX:+PrintAssembly,用于输出JIT后的汇编代码,虚拟机默认是Client。
  for (int i = 0, n = list.size(); i < n; i++)的循环体是
  0x01fcd554: inc    %edx               ; OopMap{[60]=Oop off=245}                                        ;*if_icmplt                                        ; - Client1::main@63 (line 17)  0x01fcd555: test   %eax,0x1b0100      ;   {poll}  0x01fcd55b: cmp    %eax,%edx  ;;  124 branch [LT] [B5]   0x01fcd55d: jl     0x01fcd554         ;*if_icmplt                                        ; - Client1::main@63 (line 17)

  变量i放在edx中,变量n放在eax中,inc指令对应i++(其实被优化成++i了),test指令是在回边处进行safepoint轮询(safepoint是用于进入GC,停顿线程),cmp是比较n和i的值,jl就是当i<n的时候进行跳转,跳转的地址是回到inc指令。
  for (int i = 0; i < list.size(); i++)的循环体是:
  0x01b6d610: inc    %esi  ;;  block B7 [110, 118]  0x01b6d611: mov    %esi,0x50(%esp)  0x01b6d615: mov    0x3c(%esp),%esi  0x01b6d619: mov    %esi,%ecx          ;*invokeinterface size                                        ; - Client1::main@113 (line 23)  0x01b6d61b: mov    %esi,0x3c(%esp)  0x01b6d61f: nop      0x01b6d620: nop      0x01b6d621: nop      0x01b6d622: mov    $0xffffffff,%eax   ;   {oop(NULL)}  0x01b6d627: call   0x01b2b210         ; OopMap{[60]=Oop off=460}                                        ;*invokeinterface size                                        ; - Client1::main@113 (line 23)                                        ;   {virtual_call}  0x01b6d62c: nop                       ; OopMap{[60]=Oop off=461}                                        ;*if_icmplt                                        ; - Client1::main@118 (line 23)  0x01b6d62d: test   %eax,0x160100      ;   {poll}  0x01b6d633: mov    0x50(%esp),%esi  0x01b6d637: cmp    %eax,%esi  ;;  224 branch [LT] [B8]   0x01b6d639: jl     0x01b6d610         ;*if_icmplt                                        ; - Client1::main@118 (line 23)

  可以看到,除了上面原有的几条指令外,确实还多了一次invokeinterface方法调用(这里发生了方法调用,但是基本上没有分派的开销,因为inline cache是能起作用的),执行的方法是size,方法接收者是list对象,除此之外,其他指令都和上面的循环体一致。所以至少在HotSpot Client VM中,第一种循环的写法是能提高性能的。

  但是这个结论并不是所有情况都能成立,譬如这里把list对象从ArrayList换成一个普通数组,把list.size()换成list.length。那将可以观察到两种写法输出的循环体是完全一样的(都和前面第一段汇编的循环一样),因为虚拟机不能保证ArrayList的size()方法调用一次和调用N次是否会产生不同的影响,但是对数组的length属性则可以保证这一点。也就是for (int i = 0, n = list.length; i < n; i++)和for (int i = 0; i < list.length; i++)的性能是没有什么差别的。

  再来看看ServerVM和ClientVM中这2段代码JIT后的差别,本想直接对比汇编代码,但ServerVM经过Reordering后,代码就完全混乱了,很难和前面的比较,不过我们还是可以注意到两者编译过程的不同:

  这是Client VM的编译过程
VM option '+PrintCompilation'    169   1       java.lang.String::hashCode (67 bytes)    172   2       java.lang.String::charAt (33 bytes)    174   3       java.lang.String::indexOf (87 bytes)    179   4       java.lang.Object::<init> (1 bytes)    185   5       java.util.ArrayList::add (29 bytes)    185   6       java.util.ArrayList::ensureCapacityInternal (26 bytes)    186   1%      Client1::main @ 21 (79 bytes)

  这是Server VM的编译过程
VM option '+PrintCompilation'    203   1       java.lang.String::charAt (33 bytes)    218   2       java.util.ArrayList::add (29 bytes)    218   3       java.util.ArrayList::ensureCapacityInternal (26 bytes)    221   1%      Client1::main @ 21 (79 bytes)    230   1%     made not entrant  Client1::main @ -2 (79 bytes)    231   2%      Client1::main @ 51 (79 bytes)    233   2%     made not entrant  Client1::main @ -2 (79 bytes)    233   3%      Client1::main @ 65 (79 bytes)

  ServerVM中OSR编译发生了3次,丢弃了其中2次(made not entrant的输出),换句话说,就是main()的每个循环JIT编译器都要折腾一下子。当然这并不是ServerVM看起来比ClientVM看起来慢的唯一原因。ServerVM的优化目的是为了长期执行生成尽可能高度优化的执行代码,为此它会进行各种努力:譬如丢弃以前的编译成果、在解释器或者低级编译器(如果开启多层编译的话)收集性能信息等等,这些手段在代码实际执行时是必要和有效的,但是在microbenchmark中就会显得很多余并且有副作用。因此写microbenchmark来测试Java代码的性能,经常会出现结果失真。
正好你在用fastdebug版,可以加个-XX:+TraceICs来看看。

HotSpot client compiler只利用了CHA来做虚方法调用的优化,而JDK初始化的时候就会加载ArrayList和LinkedList,从CHA来看List接口有多个实现,所以无法只通过CHA证明list.size()只有单一可能调用的目标,就只能用inline cache而不能inline了。

你想说第一种更快吧…

话说你的编译日志和帖出来的源码不对应嘛。行号对不上 = =import java.util.ArrayList;import java.util.List;public class Client2 {public static void doTest() {List<Object> list = new ArrayList<Object>();Object obj = new Object();// 填充数据for (int i = 0; i < 1000000; i++) {list.add(obj);}long start;start = System.nanoTime();// 初始化时已经计算好条件for (int i = 0, n = list.size(); i < n; i++) {}System.out.println("判断条件中计算:" + (System.nanoTime() - start) + " ns");start = System.nanoTime();// 在判断条件中计算for (int i = 0; i < list.size(); i++) {}System.out.println("判断条件中计算:" + (System.nanoTime() - start) + " ns");} public static void main(String[] args) {doTest();doTest();doTest();}}

Icy再来分析分析看?
提示:正文里client compiler没把list.size()内联掉,结合这个Client2版本来看又如何呢?猜猜看是什么东西导致了差异?
关键词有两个,一个是三个字母的缩写,另一个是“phi” ;; block B8 [118, 121] 0x00bbe448: inc %esi ; OopMap{edi=Oop [48]=Oop off=1017} ;*goto ; - Client2::doTest@121 (line 22) 0x00bbe449: test %eax,0x970100 ;*goto ; - Client2::doTest@121 (line 22) ; {poll} ;; block B7 [107, 115] 0x00bbe44f: mov 0xc(%edi),%ecx ;*getfield size ; - java.util.ArrayList::size@1 (line 177) ; - Client2::doTest@110 (line 22) 0x00bbe452: cmp %ecx,%esi ;; 240 branch [LT] [B8] 0x00bbe454: jl 0x00bbe448 ;*if_icmpge ; - Client2::doTest@115 (line 22)
这是Client2的“第二个版本”的for循环的循环体被标准编译后生成的代码。分析交给你了 ^_^
嗯顺便一提,这种情况下仍然是“第一种”的代码快些。不过差别比Client1里看到的状况小多了。
list没挂啊。那段代码里%edi就是list局部变量分配到的寄存器。
0xc(%edi)

这用Intel语法写就是
[edi + c]

也就是
[list + #offset_of_size_field]

换回成Java语法那就是
list.size
List<Object> list = flag ? new LinkedList<Object>() : new ArrayList<Object>();
对应的doTest()改为doTest(boolean flag),JIT的输出就立即变成:
  0x0267df4c: inc    %esi  ;;  block B10 [124, 132]  0x0267df4d: mov    %esi,0x5c(%esp)  0x0267df51: mov    0x40(%esp),%eax  0x0267df55: mov    %eax,%ecx          ;*invokeinterface size                                        ; - Client2::doTest@127 (line 24)  0x0267df57: mov    %eax,0x40(%esp)  0x0267df5b: nop      0x0267df5c: nop      0x0267df5d: nop      0x0267df5e: mov    $0xffffffff,%eax   ;   {oop(NULL)}  0x0267df63: call   0x0263b210         ; OopMap{[64]=Oop off=1080}                                        ;*invokeinterface size                                        ; - Client2::doTest@127 (line 24)                                        ;   {virtual_call}  0x0267df68: nop                       ; OopMap{[64]=Oop off=1081}                                        ;*if_icmplt                                        ; - Client2::doTest@132 (line 24)  0x0267df69: test   %eax,0x200100      ;   {poll}  0x0267df6f: mov    0x5c(%esp),%esi  0x0267df73: cmp    %eax,%esi  ;;  256 branch [LT] [B11]   0x0267df75: jl     0x0267df4c         ;*if_icmplt

又变回了老样子,因为这时候虚拟机已经无法推断出list的实际类型了,alright,那我猜那3个字母的组合是“Exact Type Inference”——精确类型推断。

List<Object> list = flag ? new LinkedList<Object>() : new ArrayList<Object>(); 那在标准编译中,list的类型也无法精确推断出来。

由HotSpot client compiler在OSR编译时转换为HIR之后的控制流图:

这B1前的入口就是真正带来差异的点。OSR入口使得B1的开头不得不带上了若干个phi节点,破坏了原本的类型信息的准确性。

热点排行