それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

JIT コンパイラのコンパイラスレッド、コード最適化について

はじめに

今回は前回の内容に引き続きJIT コンパイラについての話。割と薄くなる予定。
コンパイラスレッドとか最適化とかそのあたり。
Java パフォーマンスには、 JVM エンジニアが JVM の振る舞いを検証するために利用したり、コンパイルに興味を持った人が読むと良いとある。

コンパイラのスレッド

前回の記事で、コンパイルには閾値があると書いたがそれに関連している。
JVM ではコンパイル対象になったメソッドやループはキューに配置され、バックグラウンドで動く一つ以上のスレッドによって非同期プロセスとしてコンパイルが行われる。
そのためコードを実行中であってもコンパイルすることが可能となる。
またその時に利用されるキューは FIFO ではなく、呼び出しカウンタの値が大きいメソッドが優先的に処理される。

デフォルトでは C1 コンパイラはスレッドが一つで、C2 コンパイラはスレッドを二つ利用する。
この値は -XX:CICompilerCount=N で合計数を指定することでスレッド数を制御できる。

コンパイラのスレッドは単一 CPU マシン上であれば、一つにしてしまい競合状態を減らす方がパフォーマンス的には良いとされているがそれはウォームアップ時に大きなメリットがあるだけで多くのホットスポットコンパイルされたあとではメリットが少ない。

また階層型コンパイルを利用している環境下ではスレッド数を多くするとコンパイルスレッドも増えシステム全体に影響を及ぼす場合がある。
この時はスレッド数を減らした方がウォームアップの期間が長くなるというデメリットはあるものの全体のスループットを向上させることができる。

あまり使わないとは思うけど。

コードの最適化

インライン化

以下のコードを考える。

public class Point {
    private int x, y;
    public int getX() { return x; }
    public void setX(int i) { x = i; }
}
Point p = getPoint();
p.setX(p.getX() * 2);

このように呼び出されるコードがある場合、次のようにインライン化される。

Point p = getPoint();
p.x = p.x * 2;

これによりメソッド呼び出しが省略されるため、処理速度が向上する。
これまでのコードキャッシュやスレッドとは異なり、インライン化が行われる場合の振る舞いを見る術がない。
しかし、JVMソースコードビルドするのであれば -XX:+PrintInlining というフラグを利用可能にすることもできる。

インライン化するかどうかの判断基準はメソッドがよく利用されるかどうかとそれ自体のサイズに大きく影響される。
そのため、 -XX:MaxFreqInlineSIze, -XX:MaxInlineSize でインライン化するメソッドのサイズを調整することもできる。

エスケープ分析

次はエスケープ分析について。
これはデフォルトで有効になっていて、これが利用できる場合は積極的に最適化を行う。
最適化を行う項目はいろいろとあるらしいが、書籍に書かれているものであればループ内でのみ特定のオブジェクトが利用されている場合
同期ロックを取らないようにしたり、フィールドの値をメモリに持つ必要がなければレジスタに配置したり、オブジェクト自体が各フィールドだけ管理するようになったりと様々。

これはシンプルなコードだけでなく複雑なコードに対しても同様に適用される。
最適化がうまくいかない場合は、JVM のバグであることが多いそうだが、一番簡単で効果のある対処法として対象のコードをシンプルにすることが有効らしい。

おわりに

JIT コンパイルの話はいったんここまで。
次は GC とか。