#chiroito ’s blog

Java を中心とした趣味の技術について

JITとコードの暖気の実体

どうも、趣味でOpenJDKのコミッタをしてます。

とあるブログを読んでいたら気になる点があったので検証してみました。

JITと暖気

Javaプロセスはアプリケーションを動かしながら必要に応じてバックグラウンドでバイトコードをネイティブコードにコンパイルします。このコンパイル時にはCPUリソースを使用します。

コンパイルにはいくつかのレベルがありますが、コンパイルされる前やレベルの低いコンパイルのコードはCPUのリソース効率が悪かったり、アプリケーションの処理中にコンパイルが実行されるとCPUリソースを奪いあったりなどが問題になります。

そのため、Java のアプリケーションで性能を気にする要件がある場合、本番に近いリクエストを投げてコードをJITコンパイルする事があります。これをよく暖気と言います。これにより本番のリクエストが来る前にコードを最適化し、よりCPUリソース効率の高いコードで本番のリクエストを迎え撃ちます。

暖気の対象は?

アプリケーションを動かすときには、以下のように 4 つの分類のコードが実装されます。

  • アプリケーションコード
  • フレームワークコード
  • Java標準API
  • JDKの内部コード

アプリケーションコードはみなさんが実際に書くコードです。これはフレームワークが公開しているAPIとJavaの標準APIを組み合わせて実装します。フレームワークコードはアプリ開発者のために公開しているAPIとフレームワークの内部となる実装があります。これらは依存関係のある他のフレームワークとJava標準APIで実装されています。Java標準APIはJavaに含まれている物です。JDKの内部コードはアプリケーション開発者が使えないJVMの内部で実行されるコードです。

暖機をする場合、これら 4 種類のコードがコンパイルされる必要があります。

検証方法

では、どれくらいリクエストを投げれば良いのでしょうか?サンプルアプリケーションを実装してみました。そのアプリケーションは 1 つの API を持ち、その中では以下のような処理をします。

  • REST で別の API へリクエスト
  • Database に Insert 処理

今回検証した環境は以下のとおりです。

  • SpringBoot 2.3.4
  • OpenJDK 14 (SpringBoot 2.3.4がサポートする最新)

そのアプリケーションを起動し API に 20,000 回リクエストを投げました。コンパイルは一般的に一番高速なサーバコンパイルされたメソッドだけを計測しています。

結果

次のグラフは1,000回毎にコンパイルされたメソッドの累計を示しています。

f:id:chiroito:20200918215921p:plain

リクエストが 0 回というのはアプリケーションが起動してからリクエストを投げる前までです。この段階で 740 メソッドがコンパイルされています。最終的には 2,220 メソッドがコンパイルされていました。

簡単なアプリだったので 20,000 リクエスト投げれば JIT は収束するかなと思ったのですが、まだまだ足りなかったようです。

次のグラフは、1,000 リクエストごとに新規でコンパイルされたメソッド数の推移です。 リクエスト回数が 3,000 というところには 2,001 ~ 3,000 回目のリクエストを投げているときにコンパイルされたメソッドだけを数えています。これらのメソッドを、アプリケーション、フレームワーク、標準API、JDK毎に集計しています。

f:id:chiroito:20200918222251p:plain

これを見ると、アプリケーションが起動した時点で標準APIのコードが多くコンパイルされています。これらの内訳はString、Class、ArrayList、HashMapなど、よく使われるクラスのメソッドです。

JDKの内部コードは起動時にほぼコンパイルされています。

Java標準APIは 8,000 リクエストぐらいで収束しています。これは依存関係の最も上位にある java.base モジュールにあるメソッドで使う物は大体コンパイルが終わったことを示しています。

フレームワークも同様に 8,000 リクエストぐらいで収束して居るように見えますが、そのあとも少しずつコンパイルされています。これは、フレームワークの依存関係の上位にある物が先にコンパイルされて、たまに実行されているメソッドが後まで伸びています。

アプリケーションは今回リクエスト対象となる API と JPA のエンティティ の 2 つを作りましたが、API は 1 つ目に 7,000 リクエストでコンパイルされ、JPA のエンティティのコンストラクタが 2 つ目に 19,000 リクエストでコンパイルされました。

これらの結果から、アプリケーションがコンパイルされるのは結構遅いです。これはもちろん実装にもよります。

注意していただきたいのは、API ごとに 7,000 リクエスト投げればOK。ではなく、アプリケーションでコンパイルしたいメソッドがコンパイルされたのをキチンと確認してから Rediness Probe で OK を返すようにしましょう。

どのようにやれば良いかは別のブログで近々書きます。