RE:循環文の書き方、ClientとServerの性能の違い及びmicrobennchmarkの不正確さ


もっと読む
この文章もレスです。ブログを怠けるために続けています。
========================================================================================================
友達がこのコードをくれました。1.for(int=0,n=list.size();i<n;i+++)の書き方はfor(int=0;i<size+)より速いですか?2.なぜこのコードはServer VMで測定された速度はCient VMより遅いですか?
public class Client1 {
	public static void main(String[] args) {
		List list = new ArrayList();
		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();iD:\_DevSpace\jdk1.7.0\fastdebug\bin>java -version java 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)デバッグパラメータには、JIT後のアセンブリコードを出力するための-XX:+PrintAsssembleが追加されています。仮想マシンはデフォルトではClientです。
for(int i=0,n=list.size();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ポーリングを行います。cmpはnとiの値を比較します。jlはi forです。

  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 Cient VMの中で、第一の循環の書き方は性能を高めることができます。
しかし、この結論はすべての場合に成立するわけではなく、例えばここでリストオブジェクトをArayListから普通の配列に変えて、list.size()をlist.lengthに変えます。それは、2つの書き方出力の循環体が全く同じであることを観察することができる(いずれも前の第1段のアセンブリのサイクルと同じ)。仮想マシンは、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を通過した後、コードは完全に混乱しています。前との比較は難しいです。でも、二つのコンパイルの過程の違いに注意してください。
これはCient 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:: (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でOSTRコンパイルが3回発生し、そのうち2回(made not entrantの出力)を破棄しました。言い換えれば、main()の各サイクルはJITコンパイラが一気に振り回されます。もちろんこれはServerVMがClientVMより遅く見える唯一の理由ではない。ServerVMの最適化の目的は、可能な限り高度に最適化された実行コードを生成するための長期的な実行であり、そのためには、以前のコンパイルの成果を破棄したり、インタプリタやローレベルのコンパイラ(複数のコンパイルを開くと)で性能情報を収集するなど、コードの実際の実行に必要かつ効果的な手段である。しかし、microbennchmarkには多くの余剰が見られ、副作用があります。そのため、microbennchmarkを書いてJavaコードの性能をテストします。結果がよく歪みます。