Java HotSpot VMでのJITコンパイル


Java HotSpot仮想マシンはOracleがSunを買収したときに入手したもので、JVMとオープンソースのOpenJDKはこの仮想マシンをベースに発展しています.他の仮想マシンと同様に、HotSpot仮想マシンはバイトコードにランタイム環境を提供します.実際には、主にこの3つのことをします.
  • は、方法によって要求された命令および演算を実行する.
  • 新しいタイプ(すなわちクラスロード)を位置決め、ロード、検証します.
  • はアプリケーションメモリを管理します.

  • 最後の2点はそれぞれの分野の大きな話題であるため,この文章ではコード実行のみに注目する.
    JITコンパイル
    Java HotSpotは、バイトコードを解釈したり、ローカルマシンコードにコンパイルしたりして、より速く実行できるハイブリッドモードの仮想マシンです.-XX:+PrintCompilationパラメータを構成することで、JITによってコンパイルされた方法の情報をlogファイルに表示できます.JITコンパイルは、実行時--メソッドが複数回実行された後に発生します.メソッドを使用する必要がある場合、HotSpot VMはこれらのコードを最適化する方法を決定します.
    JITコンパイルによるパフォーマンスの向上が気になる場合は、-Djava.compiler=noneを使用してオフにし、ベンチマークテストプログラムを実行して違いを確認します.
    Java HotSpot仮想マシンは、クライアントまたはserverの2つのモードで実行できます.JVM起動時に-clientまたは-serverのオプションを設定することで、いずれかを選択できます.両方のモードにはそれぞれ適用シーンがありますが、本稿ではserverモードにのみ触れます.
    2つのモードの最も主要な違いは、serverモードでより急進的な最適化が行われることです.これらの最適化は、永遠に真実ではない仮定の上に構築されています.単純な保護条件(guard condition)は、最適化が常に正しいことを保証するために、これらの仮定が成立しているかどうかを検証します.仮定が成立しない場合、Java HotSpot仮想マシンは最適化を取り消し、解釈モードに戻ります.つまり、Java HotSpot仮想マシンは、仮定が成立しないために誤った動作を示すことなく、最適化がまだ有効であるかどうかを常にチェックします.
    Serverモードでは、Java HotSpot仮想機会のデフォルトでは、解釈モードでメソッドが10000回実行され、JITコンパイルがトリガーされます.この値は、仮想マシンパラメータ-XX:CompileThresholdによって調整することができる.例えば-XX:CompileThreshold=5000は、JITコンパイルをトリガするメソッドの実行回数を半分に減らす.(注:JITトリガ条件については、『Java仮想マシンを深く理解する』第11章および『Java Performance』第3章HotSpot VM JIT Compilersセクションを参照)
    これにより、初心者がコンパイルしきい値を非常に低い値に調整する可能性があります.しかし、この誘惑に抵抗するには、仮想マシンのパフォーマンスが低下する可能性があるため、最適化後に減少した方法の実行時間は、JITコンパイルにかかる時間を相殺するのに十分ではありません.
    Java HotSpot仮想機能がJITコンパイルのために十分な統計情報を収集する場合、パフォーマンスが最も優れています.コンパイルしきい値を下げると、Java HotSpot仮想マシンはホットスポット以外のコードのコンパイルに多くの時間を費やす可能性があります.一部の最適化は、統計が十分に収集されている場合にのみ行われるため、コンパイルしきい値を低減すると最適化の効果が低下する可能性があります.
    一方,多くの開発者は,コンパイルモードでできるだけ早くより良い性能を得るための重要な方法を望んでいる.
    この問題を解決するには、通常、プロセスが開始された後、コードが強制的にコンパイルされるように予熱されます.受注システムや取引システムのように、予熱が実際の受注を生じないことを確保することが重要です.
    Java HotSpot仮想マシンは、JITのコンパイル情報を出力するために多くのパラメータを提供しています.最もよく使われるのは、前述したPrintCompilationであり、他にもいくつかのパラメータがあります.
    次に、PrintCompilationを使用して、Java HotSpot仮想マシンの実行時のコンパイル方法の効果を観察します.まず、時間を計るシステムについてお話しする必要があります.nanoTime()メソッド.
    タイミングメソッド
    Javaは私たちに2つの主要な時間値を取得する方法を提供した:currentTimeMillis()とnanoTime().前者は,我々が実体世界で見た時間(いわゆる時計時間)に対応し,その精度は多くの場合を満たすことができるが,低遅延の応用には適用されない.
    ナノ秒タイマはより高い精度を有する.このタイマーは時間の間隔が極めて短い.1ナノ秒は、光が光ファイバの中で20 CM移動するのに要する時間であり、それに比べて、光が光ファイバを介してロンドンからニューヨークに伝送されるのに約27.5ミリ秒かかる.
    ナノ秒級のタイムスタンプの精度が高すぎると、不適切な使用で大きな誤差が生じるため、使用時に注意が必要です.
    例えば、currentTimeMillis()はマシン間でうまく同期でき、ネットワーク遅延の測定に使用できるが、nanoTime()はマシン間で使用できない.
    次に、上記の理論を実践して、簡単な(しかし極めて強い)JITコンパイル技術を見てみましょう.
    メソッドインライン
    メソッドインラインはコンパイラ最適化の重要な手段の一つである.メソッドインラインとは,メソッドのコードを呼び出しを開始するメソッドに「コピー」し,メソッド呼び出しを除去することである.この機能は、小さなメソッドを呼び出すよりも時間がかかる可能性があるため、非常に重要である.
    JITコンパイラは漸進的にインラインすることができ,開始時にインラインする簡単な方法であり,他の最適化が可能であれば,インライン後の大きなコードブロックを最適化する.
    Listing 1,Listing 1 AおよびListing 1 Bは簡単なテストであり,直接操作フィールドとgetter/setter法による比較を行った.単純なgettersメソッドとsettersメソッドがインラインを使用していない場合、メソッド呼び出しは直接操作フィールドよりもコストが高いため、それらを呼び出すコストはかなり大きい.
    Listing1:
    public class Main {
        private static double timeTestRun(String desc, int runs, 
            Callable callable) throws Exception {
            long start = System.nanoTime();
            callable.call();
            long time = System.nanoTime() - start;
            return (double) time / runs;
        }
    
        // Housekeeping method to provide nice uptime values for us
        private static long uptime() {
            return ManagementFactory.getRuntimeMXBean().getUptime() + 15; 
        // fudge factor
        }
    
        public static void main(String... args) throws Exception {
            int iterations = 0;
            for (int i : new int[]
                { 100, 1000, 5000, 9000, 10000, 11000, 13000, 20000, 100000} ) {
                final int runs = i - iterations;
                iterations += runs;
    
                // NOTE: We return double (sum of values) from our test cases to
                // prevent aggressive JIT compilation from eliminating the loop in
                // unrealistic ways
                Callable directCall = new DFACaller(runs);
                Callable viaGetSet = new GetSetCaller(runs);
    
                double time1 = timeTestRun("public fields", runs, directCall);
                double time2 = timeTestRun("getter/setter fields", runs, viaGetSet);
    
                System.out.printf("%7d %,7d\t\tfield access=%.1f ns, getter/setter=%.1f ns%n",
                    uptime(), iterations, time1, time2);
                // added to improve readability of the output
                Thread.sleep(100);
            }
        }
    }
    

    Listing1A:
    public class DFACaller implements Callable{
        private final int runs;
    
        public DFACaller(int runs_) {
            runs = runs_;
        }
    
        @Override
        public Double call() {
            DirectFieldAccess direct = new DirectFieldAccess();
            double sum = 0;
            for (int i = 0; i < runs; i++) {
                direct.one++;
                sum += direct.one;
            }
            return sum;
        }
    }
    
    public class DirectFieldAccess {
        int one;
    }
    

    Listing1B:
    public class GetSetCaller implements Callable {
        private final int runs;
    
        public GetSetCaller(int runs_) {
            runs = runs_;
        }
    
        @Override
        public Double call() {
            ViaGetSet getSet = new ViaGetSet();
            double sum = 0;
            for (int i = 0; i < runs; i++) {
                getSet.setOne(getSet.getOne() + 1);
                sum += getSet.getOne();
            }
            return sum;
        }
    }
    
    public class ViaGetSet {
        private int one;
    
        public int getOne() {
            return one;
        }
    
        public void setOne(int one) {
            this.one = one;
        }
    }
    
    java -cp. -XX:PrintCompilation Mainを使用してテストケースを実行すると、パフォーマンスの違いが表示されます(Listing 2を参照).
    Listing2
     31    1     java.lang.String::hashCode (67 bytes) 
     36   100    field access=1970.0 ns, getter/setter=1790.0 ns 
     39    2     sun.nio.cs.UTF_8$Encoder::encode (361 bytes) 
     42    3     java.lang.String::indexOf (87 bytes) 
    141   1,000 field access=16.7 ns, getter/setter=67.8 ns 
    245   5,000 field access=16.8 ns, getter/setter=72.8 ns 
    245    4     ViaGetSet::getOne (5 bytes) 
    348   9,000 field access=16.0 ns, getter/setter=65.3 ns 
    450    5     ViaGetSet::setOne (6 bytes) 
    450  10,000 field access=16.0 ns, getter/setter=199.0 ns 
    553    6     Main$1::call (51 bytes) 
    554    7     Main$2::call (51 bytes) 
    556    8     java.lang.String::charAt (33 bytes) 
    556  11,000 field access=1263.0 ns, getter/setter=1253.0 ns 
    658  13,000 field access=5.5 ns, getter/setter=1.5 ns 
    760  20,000 field access=0.7 ns, getter/setter=0.7 ns 
    862 100,000 field access=0.7 ns, getter/setter=0.7 ns 
    

    これはどういう意味ですか.Listing 2の最初の列は、プログラムが文の実行に起動したときに経過したミリ秒数であり、2番目の列は、メソッドID(コンパイルされたメソッド)または遍歴回数である.
    注意:StringとUTF_はテストで直接使用されていません.8クラスですが、プラットフォームが使用しているため、コンパイルされた出力にはまだ表示されます.
    Listing 2の2行目から、直接アクセスフィールドもgetter/setterも遅いことがわかります.これは、1回目の実行時にクラスロードの時間が含まれているため、次の行が速くなります.コードがコンパイルされていないにもかかわらず.
    また、次の点に注意してください.
  • 1000および5000回の遍歴では、getter/setterメソッドを使用するよりも、getterおよびsetterがインラインまたは最適化されていないため、フィールドを直接操作する方が速い.それでもかなり速いです
  • 9000回ループすると、getterメソッドが最適化され(ループごとに2回呼び出されるため)、パフォーマンスがわずかに向上する.
  • は10000回の遍歴でsetterメソッドも最適化され、最適化に時間がかかるため、実行速度が低下した.
  • 最終的には、2つのテストクラスが最適化されました.
  • DFACAllerはフィールドを直接操作し、GetSetCallerはgetterメソッドとsetterメソッドを使用します.このとき、それらは最適化されたばかりでなく、内蔵されています.
  • 次の遍歴から、テスト例の実行時間は依然として最も速くないことがわかる.

  • は13000回の遍歴の後、両方のフィールドアクセス方式の性能は最後のより長い時間のテストの結果と同じように良く、性能の安定した状態に達した.

  • 特に、直接アクセスフィールドとgetter/setterアクセスによる安定した状態でのパフォーマンスは、方法がGetSetCallerに組み込まれているため、すなわちviaGetSetで行われていることはdirectCallと全く同じであることに注意してください.
    JITコンパイルはバックグラウンドで行います.使用可能な最適化手段は、ランダムに異なる場合があり、同じプログラムの複数回の実行期間も異なる場合があります.
    まとめ
    この文章では、JITがコンパイルした氷山の一角について説明していますが、特に、プラットフォームのダイナミック性に翻弄されないように、良い基準テストを書く方法と統計情報をどのように使用するかについては言及していません.
    ここで使用するベンチマークテストは非常に簡単で、実際のベンチマークテストには向いていません.第2部では、実際のベンチマークテストを示し、JITコンパイルのプロセスを継続する予定です.
    原文Introduction to JIT Compliation in Java Hotspot VM翻訳者郭蕾校正丁一via ifeve