Java のJIT コンパイルについて
JIT コンパイルとは
Java のコードは javac コマンドでコンパイルすると中間バイトコードに変換され、それを JVM が解釈することによってアプリケーションが実行される。
中間バイトコードは JVM が解釈できる形になるため、Java はインタープリター言語のようなプラットフォームにおける独立性を確保している。
この後 JVM はインタープリターのように中間バイトコードを解釈するが、その時に JVM を実行しているプラットフォーム毎にバイナリにコンパイルされる場合がある。
これを Just-In-Time コンパイルという。
JIT コンパイルによる効果を確かめる
エラトステネスの篩を使って素数を求める以下のコードを利用する。
public class SampleAlgorithm { private static final int LIMIT = 1000000; public static void main(String... args) { int primeCount = 2; boolean isPrime = false; for (int i = 5; i <= LIMIT; i++) { for (int j = 2; j * j <= i; j++) { if (i % j == 0) { isPrime = false; break; } isPrime = true; } if (isPrime) { primeCount += 1; } } System.out.println(primeCount); } }
このコードを -Xint オプションをつけた場合、つけなかった場合で処理時間を比較する。
❯ time java -Xint SampleAlgorithm.java 78498 java -Xint SampleAlgorithm.java 1.80s user 0.05s system 98% cpu 1.876 total
❯ time java SampleAlgorithm.java 78498 java SampleAlgorithm.java 1.14s user 0.07s system 160% cpu 0.753 total
1000000 までの素数であれば微々たる差だが、桁を増やすと差が明確になっていく。
1億以下の素数を計算すると以下のようになる。
❯ time java -Xint SampleAlgorithm.java 664579 java -Xint SampleAlgorithm.java 21.35s user 0.13s system 99% cpu 21.590 total
❯ time java SampleAlgorithm.java 664579 java SampleAlgorithm.java 6.18s user 0.08s system 108% cpu 5.769 total
JIT コンパイルの動作
実行時のオプションに -XX:+PrintCompilation をつけることで JIT コンパイルが何を行っているかわかる。
このフォーマットには列ごとに意味があるが、それは次の JIT コンパイラのチューニング part1 でまとめる。
❯ java -XX:+PrintCompilation SampleAlgorithm.java ..... 530 958 3 com.sun.tools.javac.tree.JCTree::hasTag (14 bytes) 530 957 1 com.sun.tools.javac.code.Type$JCPrimitiveType::isPrimitive (2 bytes) 530 959 3 com.sun.tools.javac.code.Type$ClassType::accept (9 bytes) 532 960 3 java.lang.invoke.DirectMethodHandle::make (263 bytes) 533 961 3 java.lang.invoke.MemberName::isField (7 bytes) 533 962 3 java.util.ArrayList::isEmpty (13 bytes) 534 963 3 com.sun.tools.javac.jvm.Code::typecode (141 bytes) 534 964 3 com.sun.tools.javac.jvm.Code::width (42 bytes) 534 965 3 com.sun.tools.javac.jvm.Code::width (16 bytes) 535 966 3 com.sun.tools.javac.code.Type::typeNoMetadata (19 bytes) 536 734 4 java.lang.StringLatin1::indexOf (121 bytes) made not entrant 537 309 3 jdk.internal.jimage.ImageStringsReader::stringFromByteBuffer (28 bytes) made not entrant 537 828 4 java.lang.ref.Reference::reachabilityFence (1 bytes) 537 169 3 java.lang.ref.Reference::reachabilityFence (1 bytes) made not entrant 537 814 4 com.sun.tools.javac.util.ListBuffer::toList (10 bytes) 537 635 3 com.sun.tools.javac.util.ListBuffer::toList (10 bytes) made not entrant 537 755 4 java.lang.String::toString (2 bytes) 537 480 3 java.lang.String::toString (2 bytes) made not entrant 537 967 % 3 SampleAlgorithm::main @ 15 (65 bytes) 538 968 3 SampleAlgorithm::main (65 bytes) 538 969 % 4 SampleAlgorithm::main @ 15 (65 bytes) 539 967 % 3 SampleAlgorithm::main @ 15 (65 bytes) made not entrant 745 969 % 4 SampleAlgorithm::main @ 15 (65 bytes) made not entrant
- XX:+LogCompilation をつけるとより詳細な以下のようなログを確認することができる。
<<<一部抜粋>>> <nmethod compile_id='1' compiler='c1' level='3' entry='0x000000011d1911c0' size='1056' address='0x000000011d191010' relocation_offset='376' insts_offset='432' stub_offset='816' scopes_data_offset='872' scopes_pcs_offset='936' dependencies_offset='1032' nul_chk_table_offset='1040' metadata_offset='864' method='java.lang.StringLatin1 hashCode ([B)I' bytes='42' count='101' backedge_count='2096' iicount='101' stamp='0.054'/> <writer thread='9731'/> <task_queued compile_id='2' method='java.util.concurrent.ConcurrentHashMap tabAt ([Ljava/util/concurrent/ConcurrentHashMap$Node;I)Ljava/util/concurrent/ConcurrentHashMap$Node;' bytes='22' count='256' iicount='256' level='3' stamp='0.054' comment='tiered' hot_count='256'/> <task_queued compile_id='3' method='jdk.internal.misc.Unsafe getObjectAcquire (Ljava/lang/Object;J)Ljava/lang/Object;' bytes='7' count='256' iicount='256' level='3' stamp='0.054' comment='tiered' hot_count='256'/> <task_queued compile_id='4' method='java.lang.String coder ()B' bytes='15' count='256' iicount='256' level='3' stamp='0.054' comment='tiered' hot_count='256'/> <writer thread='42755'/> <nmethod compile_id='2' compiler='c1' level='3' entry='0x000000011d191660' size='1376' address='0x000000011d191490' relocation_offset='376' insts_offset='464' stub_offset='1168' scopes_data_offset='1240' scopes_pcs_offset='1288' dependencies_offset='1368' oops_offset='1216' metadata_offset='1224' method='java.util.concurrent.ConcurrentHashMap tabAt ([Ljava/util/concurrent/ConcurrentHashMap$Node;I)Ljava/util/concurrent/ConcurrentHashMap$Node;' bytes='22' count='260' iicount='260' stamp='0.055'/> <writer thread='9731'/> <task_queued compile_id='5' method='java.lang.String isLatin1 ()Z' bytes='19' count='256' iicount='256' level='3' stamp='0.055' comment='tiered' hot_count='256'/> <writer thread='42755'/> <nmethod compile_id='3' compiler='c1' level='3' entry='0x000000011d191bc0' size='856' address='0x000000011d191a10' relocation_offset='376' insts_offset='432' stub_offset='720' scopes_data_offset='776' scopes_pcs_offset='800' dependencies_offset='848' metadata_offset='768' method='jdk.internal.misc.Unsafe getObjectAcquire (Ljava/lang/Object;J)Ljava/lang/Object;' bytes='7' count='263' iicount='263' stamp='0.055'/> <nmethod compile_id='5' compiler='c1' level='3' entry='0x000000011d191f40' size='880' address='0x000000011d191d90' relocation_offset='376' insts_offset='432' stub_offset='752' scopes_data_offset='808' scopes_pcs_offset='824' dependencies_offset='872' metadata_offset='800' method='java.lang.String isLatin1 ()Z' bytes='19' count='259' iicount='259' stamp='0.055'/> <nmethod compile_id='4' compiler='c1' level='3' entry='0x000000011d1922c0' size='816' address='0x000000011d192110' relocation_offset='376' insts_offset='432' stub_offset='688' scopes_data_offset='744' scopes_pcs_offset='760' dependencies_offset='808' metadata_offset='736' method='java.lang.String coder ()B' bytes='15' count='262' iicount='262' stamp='0.055'/> <writer thread='9731'/> <task_queued compile_id='6' method='java.lang.Object <init> ()V' bytes='1' count='512' iicount='512' level='3' stamp='0.057' comment='tiered' hot_count='512'/> <writer thread='42755'/>
この時に重要になるのが level の値。これは階層型コンパイルが行われ、その際にどのコンパイルが適用されたかを示している。
レベルは以下の通り。
- Level0: インタープリター実行
- Level1: プロファイルなしのクライアントコンパイル
- Level2: プロファイルありの制限付きクライアントコンパイル
- Level3: プロファイルありのクライアントコンパイル
- Level4: サーバコンパイル
クライアントコンパイルとサーバコンパイル
最近の JVM は JIT でクライアントコンパイルとサーバコンパイル*1を併用する。
クライアントコンパイルとサーバコンパイルの違いはコンパイルをどの程度積極的に行うかにあり、クライアントコンパイラは可能な限り該当コードをコンパイルし、サーバコンパイラはインタープリター的に実行しつつ実行しているコードについての情報を用いて最適化したコンパイルを行う。
JVM によっては -client, -server, -d64 オプションをつけることで片方を明示的に指定できる。(あまり使うことはないと思うが)
最近の JVM だとこの辺りがベンダー、プラットフォーム依存になっていて -client を指定してもサーバコンパイルが適用される場合もある。
おわりに
JIT コンパイラとは、という事に触れたので次は JIT コンパイラのチューニングについて書いていく
参考文献
- https://www.oracle.com/technetwork/jp/tutorials/java-mj12-architect-1683422-ja.pdf
- https://www.oracle.com/webfolder/technetwork/jp/javamagazine/Java-MA16-JIT.pdf
- OpenJDKのJIT解析用オプション - Qiita https://qiita.com/k0kubun/items/96e7b6d31c530ed8ba10
- Java HotSpot VMコマンド行オプションhttps://docs.oracle.com/javase/jp/8/docs/technotes/guides/troubleshoot/clopts001.html
- The Java® Virtual Machine Specification https://docs.oracle.com/javase/specs/jvms/se11/html/index.html
- コンパイル最適化 https://docs.oracle.com/javase/jp/10/jrockit-hotspot/compilation-optimization.htm