kagamihogeの日記

kagamihogeの日記です。

JEP 295: Ahead-of-Time Compilationをテキトーに訳した

http://openjdk.java.net/jeps/295 をテキトーに訳した

JEP 295: Ahead-of-Time Compilation

Owner    Vladimir Kozlov
Created 2016/09/15 01:20
Updated 2016/10/27 19:50
Type    Feature
Status  Targeted
Component   hotspot/compiler
Scope   Implementation
Discussion  hotspot dash compiler dash dev at openjdk dot java dot net
Effort  M
Duration    M
Priority    1
Reviewed by John Rose, Mikael Vidstedt
Endorsed by John Rose
Release 9
Issue   8166089

Summary

仮想マシンの起動前にJavaクラスをネイティブコードにコンパイルします。

Goals

  • ピーク時パフォーマンスへの影響を最小限に留めつつ、小・大規模双方のJavaアプリケーションで開始時間を改善します。
  • ユーザの作業手順の変更はなるべく最小限に抑えます。

Non-Goals

コンパイル済みコードの保存およびロードを行う公開ライブラリ的なメカニズムの提供は行いません。

Motivation

JITコンパイラは高速ですが、JITが完全にウォームアップするのに長時間を要するほど大規模なJavaプログラムとなる場合があります。低頻度使用のJavaメソッドは一度もコンパイルされない可能性があり、インタープリタ実行が繰り返されるとパフォーマンス悪化の原因となる可能性が潜在的に存在します。

Description

初期リリースではサポート対象となるモジュールはjava.baseのみとします。これは問題空間を限定するためで、その理由はjava.baseJavaコードは周知のもので、かつ、一連の内部的なテストを実行可能なためです。任意のその他のJDKやユーザーコードのAOTコンパイルは試験的なものです。

java.baseモジュールにAOTを適用するには、ユーザはモジュールをコンパイルしてJDKインストールライブラリにコピーするかJavaコマンドラインで指定します。その点以外は、AOTコンパイルされたコードの使用はエンドユーザに対して完全に透過的です。

AOTコンパイルは新規ツールのjaotcで行います。

jaotc --output libHelloWorld.so HelloWorld.class
jaotc --output libjava.base.so --module java.base

コード生成のバックエンドにはGraalを使用します。

JVM開始およびAOT初期化中に、コードは既知の場所もしくはコマンドラインのAOTLibraryフラグの指定場所から共有ライブラリの参照を試行します。もし共有ライブラリが存在する場合、それが使われます。もし共有ライブラリが見つからない場合、AOTはそのJVMインスタンスでは無効になります。

java -XX:AOTLibrary=./libHelloWorld.so,./libjava.base.so HelloWorld

java.baseにAOTライブラリをインストールしてビルドする方法を指定する新規のAOTフラグとjaotcフラグは以降のサブセクションで示します。

AOTコンパイル済みのコードに対して使われるコンテナフォーマットは共有ライブラリです。JDK 9では共有ライブラリフォーマットがELFLinux/x64のみサポートします。AOTライブラリのAOTコンパイル済みコードは、既存のCodeCacheの拡張としてJVMは扱います。

Javaクラスがロードされる時、AOTライブラリ内に対応するAOTコンパイル済コードがあればJVMはそれを参照し、Javaメソッドディスクリプタからリンクを貼ります。AOTコンパイル済コードは通常のJITコンパイルコードと同様のnvocation/deoptimization/unloadingルールに従います。

ソースコードの変更やクラス変換および再定義などにより、クラスバイトコードは変更可能なため、JVMはそうした変更を検出し、もしバイトコードがミスマッチとなる場合はAOTコンパイル済コードを除去する必要があります。これはclass fingerprintingで実現します。AOTコンパイル時に各クラスごとのfingerprintを生成して共有ライブラリのデータセクションに保存します。その後、クラスロード時にそのクラスにAOTコンパイル済コードがあると、現在のバイトコード用のfingerprintが共有ライブラリ内に保存されているものと比較されます。ミスマッチの場合、対象クラスのAOTコードは使われません。

AOT usage

AOTコンパイルの実行にはjaotcツールを用います。ツールはjavac同様にJavaインストールの一部です。

jaotc --output libHelloWorld.so HelloWorld.class

次に、アプリケーション実行時に生成したAOTライブラリを指定します。

java -XX:AOTLibrary=./libHelloWorld.so HelloWorld

今回のリリースではAOTコンパイル時と実行時に同一のJavaランタイム設定を使うようにして下さい。例えば、

jaotc -J-XX:+UseParallelGC -J-XX:-UseCompressedOops --output libHelloWorld.so HelloWorld.class 
java -XX:+UseParallelGC -XX:-UseCompressedOops -XX:AOTLibrary=./libHelloWorld.so HelloWorld

同一のJDKビルド種類(product or debug)を使用してください*1

ランタイム設定はAOTライブラリ内に記録されて、実行時にライブラリがロードされる際に検証されます。検証失敗時にはそのAOTライブラリは使われず、JVMの実行は継続するか、-XX:+UseAOTStrictLoading指定時には終了します。

JVM開始中にAOT初期化コードは既知の場所か-XX:AOTLibraryオプションが指定するライブラリ内から共有ライブラリを参照します。共有ライブラリがある場合、それが使われます。もし共有ライブラリが見つからない場合、AOTはそのJVMインスタンス実行では無効になります。

AOTライブラリは--compile-for-tieredで制御する二つのモードでコンパイルが可能です。

  • Non-tiered AOTコンパイル済みコードは、静的コンパイルをしたC++コードのような振る舞いとなり、プロファイリング情報は収集されずJITコンパイルは発生しません。
  • Tiered AOTコンパイル済みコードはプロファイリング情報を収集します。このプロファイリングはC1メソッドがTier 2でコンパイルするsimple profilingのと同じです。AOTメソッドがAOT実行スレッショールドにヒットすると、そのメソッドは完全なプロファイリング情報を収集するためにまずC1 at Tier 3で再コンパイルされます。これはC2 JITコンパイルに必要で、最適化コードを生成してピーク時アプリケーションパフォーマンスを出すために行われます。

完全プロファイリングのオーバーヘッドは、とりわけjava.baseなどのモジュールのメソッドでは、すべてのメソッドに適用するには大きすぎるため、Tier 3での再コンパイルのためには更に別のステップが必要です。ユーザーアプリケーションで、Tier 3相当のプロファイリングでAOTコンパイルを行えるようにするかもしれませんが、JDK 9ではサポートしません。

java.baseメソッドのJITコンパイルはピークパフォーマンスを得るのが望ましいため、java.baseの論理コンパイルモードはtiered AOTです。特定シナリオ下ではnon-tiered AOTコンパイルが適しています。これには予測可能な振る舞いを要求するアプリケーションや、ピークパフォーマンスよりもフットプリントの方が重要な場合や、動的なコード生成を許可しないシステムが該当します。こうしたケースの場合、AOTコンパイルはアプリケーション全体で必要であり、JDK 9で試験運用を行います。

AOTライブラリセットは異なる実行環境向けの生成が可能です。特定のランタイム設定用に生成されたjava.baseAOTライブラリに対するwell-knownな名前をJVMは解釈します*2。$JAVA_HOME/libディレクトリを参照して現在のランタイム設定に対応するライブラリをロードします。

-XX:-UseCompressedOops -XX:+UseG1GC :       libjava.base.so
-XX:+UseCompressedOops -XX:+UseG1GC :       libjava.base-coop.so
-XX:-UseCompressedOops -XX:+UseParallelGC : libjava.base-nong1.so
-XX:+UseCompressedOops -XX:+UseParallelGC : libjava.base-coop-nong1.so

以下のJavaモジュール用のAOTライブラリ名をJVMは受け付けますが、これらのコンパイル・インストール・使用は試験運用です。

jdk.compiler 
jdk.scripting.nashorn 
jdk.vm.ci 
jdk.vm.compiler

Steps to generate and use an AOT library for the java.base module

java.baseモジュールのコンパイルにはjaotcを使います。全メソッド(約50000メソッド)のコンパイル用にデータを保持するため巨大なヒープを必要とします。

jaotc -J-XX:+UseCompressedOops -J-XX:+UseG1GC -J-Xmx4g --compile-for-tiered --info --compile-commands java.base-list.txt --output libjava.base-coop.so --module java.base

java.baseのいくつかのメソッドはコンパイルに失敗するため--compile-comandsオプションで除外されています。

cat java.base-list.txt

# jaotc: java.lang.StackOverflowError
exclude sun.util.resources.LocaleNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources.TimeZoneNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources.cldr.LocaleNames.getContents()[[Ljava/lang/Object;
exclude sun.util.resources..*.LocaleNames_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.LocaleNames_.*_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.TimeZoneNames_.*.getContents\(\)\[\[Ljava/lang/Object;
exclude sun.util.resources..*.TimeZoneNames_.*_.*.getContents\(\)\[\[Ljava/lang/Object;
# java.lang.Error: Trampoline must not be defined by the bootstrap classloader
exclude sun.reflect.misc.Trampoline.<clinit>()V
exclude sun.reflect.misc.Trampoline.invoke(Ljava/lang/reflect/Method;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
# JVM asserts
exclude com.sun.crypto.provider.AESWrapCipher.engineUnwrap([BLjava/lang/String;I)Ljava/security/Key;
exclude sun.security.ssl.*
exclude sun.net.RegisteredDomain.<clinit>()V

AOTライブラリを生成したら-XX:AOTLibraryオプションでアプリケーション実行時に指定します(デフォルトではjavaはG1とJDK 9のcompressed oopsを使用するため、このフラグを指定する必要はありません)。

java -XX:AOTLibrary=./libjava.base-coop.so,./libHelloWorld.so HelloWorld

もしくは、生成したAOTライブラリをJDKインストールディレクトリにコピーします(ディレクトリのパーミッションを調整する必要があるかもしれません)。

cp libjava.base-coop.so $JAVA_HOME/lib/

上記の場合、コマンドラインで指定しなくても自動的にロードされます。

java -XX:AOTLibrary=./libHelloWorld.so HelloWorld

ライブラリサイズの縮小にはAOTライブラリから未使用シンボルの削除(strip)を検討してください。

New run-time AOT flags

-XX:+/-UseAOT

AOTコンパイル済みファイルを使います。デフォルトはONです。

-XX:AOTLibrary=<file>

AOTライブラリファイルのリストを指定します。コロン(:)あるいは(,)でそれぞれのライブラリエントリを区切ります。

-XX:+/-PrintAOT

使われるAOT klassesとメソッドを表示します。

診断に利用可能なフラグはもう一つあります(-XX:+UnlockDiagnosticVMOptionsフラグの指定が必要)。

-XX:+/-UseAOTStrictLoading

AOTライブラリの持つランタイム設定が現在のランタイム設定とマッチしない場合JVMは終了します。

JVMランタイムはJEP 158: Unified JVM Loggingで統合される以下のUnified Logging AOTタグを持ちます。

aotclassfingerprint

クラスのfingerprintがAOTライブラリに記録されているfingerprintとマッチしない場合にログを生成します。

aotclassload

対応するクラスデータがAOTライブラリ内に見つかった場合にログを生成します。

aotclassresolve

AOTコンパイル済コードからクラス解決のためのリクエストの成功・失敗ログを生成します。

jaotc: The Java Ahead-Of-Time compiler

jaotcコンパイルjavaメソッドに対してネイティブコードを生成するjava静的コンパイラです。裏側のコード生成にはGraal、.soAOTライブラリに生成にはlibelfを使用します。

このツールはjava installationの一部でjavacと同様な使い方をします。

jaotc <options> <--module name>

もしくは、

jaotc <options> <list of classes or/and jar files>

以下のjaotcフラグが使用可能です。

--module <name>

コンパイルを行うためのモジュール。

--module-path <path>

アプリケーションモジュールの参照先指定。未指定の場合JDKインストールディレクトリを参照します。

--output <file>

出力ファイル名。デフォルト名は"unnamed.so"です。

--compile-commands <file>

コンパイルコマンドにはファイル名を指定します。

exclude sun.util.resources..*.TimeZoneNames_.*.getContents\(\)\[\[Ljava/lang/Object; 
exclude sun.security.ssl.* 
compileOnly java.lang.String.*

AOTは今のところ二つのコンパイルコマンドを使用可能です。

exclude       - exclude compilation of specified methods 
compileOnly   - compile only specified methods

クラスとメソッドの指定には正規表現を使います。

--compile-for-tiered

tieredコンパイルでプロファイリングコードを生成します。デフォルトではプロファイリングコードは生成されません。

--classpath <path>

ユーザクラスファイルの参照先を指定します。

--threads <number>

コンパイルに用いるスレッド数。デフォルト値はmin(16, available_cpus)です。

--ignore-errors

クラスロード中にスローされる全ての例外を無視します。デフォルトでは、クラスロードが例外をスローする場合はコンパイルを終了します。

--exit-on-error

コンパイルエラー時に終了します。デフォルトでは、失敗したコンパイルはスキップされてそれ以外のメソッドのコンパイルは続行します。

--info

コンパイルフェーズに関する情報を表示します。

--verbose

--infoフラグ有効時にコンパイルフェーズに関するより詳しい情報を表示します。

--debug

--infoと--verboseフラグ有効時により詳しい情報を表示します。

-J<flag>

JVMランタイムシステムflagディレクトリを渡します。

--help

jaotcのメッセージとフラグを表示します。

AOT limitations in JDK 9

  • JDK 9のAOT初期リリースは64-bit javaの動作する64-bit Linuxシステムのみサポートします。
  • AOTコンパイル結果であるAOT共有ライブラリ(.so)を許可するためのlibelfがシステムにインストールされていること。
  • Javaアプリケーションで使われるAOTコードでは、同一システムもしくは同一設定システムでAOTコンパイルを実行すること。
  • JDK 9の初期リリースではAOTコンパイルがサポートするモジュールはjava.baseに限定されます。
  • 今のところG1とParallel GCのみサポートします。
  • 動的に生成されるクラスとバイトコードラムダ式, invoke dynamic)を使用するjavaコードのコンパイルはしないでください。
  • AOTコンパイルと実行には同一のJavaランタイム設定を使用して下さい。例えば、アプリケーションがAOTコードにParallel GCを使用する場合、jaotcツールでもParallel GC(-Jフラグを使用)して実行してください。

以上の制限は将来のリリースで扱う予定です。

Alternatives

プロファイルもしくはコンパイルの削減は議論され続けてきましたが、議論だけではコードをコンパイルするのにかかる時間を削減できません。別案として、低レベルIRの低速コピーを節約があり、これはさほど複雑では無さそうです(but that seems no less complex)。

Testing

AOT機能に対する新規のAOT jtregテストを開発します。

すべての既存テストがAOT有効化JDKで実行可能かテストします。既に個々のナイトリーテスト設定を実行しています*3

それ以外の設定をAOTコンパイルjava.baseモジュールを用いてAOT有効化JDK環境ですべてのテストを実行します。

Risks and Assumptions

プリコンパイルコードの使用は最適化を損なうコードとなり、結果としてパフォーマンスを下げる可能性があります。パフォーマンステストの結果では、あるアプリケーションではAOTコンパイルコードで改善が見られる一方で、明らかに悪くなるケースもありました。AOT機能はオプトインなため、ユーザアプリケーションでのパフォーマンス悪化は回避可能です。もしユーザがアプリケーション開始時間の低下に直面、あるいは、所定のピークパフォーマンスに達しない場合、-XX:-UseAOTフラグあるいはAOTライブラリの削除でAOTを無効化できます。

Dependences

AOTコンパイラはコード生成のバックエンドにGraalを使用するため、本プロジェクトはJEP 243: Java-Level JVM Compiler Interfaceに依存しています。

本プロジェクトはJDKGraal coreにマージされてLinux/x64ビルドで配布予定です。

*1:It includes the requirement to use the same JDK build variant: product or debug.が原文。色々と揃えてくれ、ってことだと思われるが、良い日本語訳思い浮かばず

*2:JVM knows next well-known names for java.base AOT libraries generated for specific runtime configuration.が原文

*3:This is already done as separate nightly testing configurations.が原文。ある特定の設定については既にナイトリービルドやってます、てこと?