フォーラム » つれづれ Java編 »
返答 (6)
高徹 JITが生成する機械語のアセンブリコードを確認したい - 高橋 徹 さんが約2ヶ月前に追加
JavaVMのJITコンパイラが生成するネイティブの機械語のアセンブリコードを見ることができれば、どのような最適化がなされているか分かります。
JavaVMには、-XX:+PrintAssembly オプションがあり、JITがコンパイルした機械語のアセンブリコードを参照する基本的な枠組みがあります。ここで、整数の除算を例に最適化を見てみます。
public static int calc(int a) {
return a / 8;
}
C:\work> java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly MulDivShift
ただし、普通にOpenJDKディストリビューションを入れただけでは、このオプションを指定しても機械語の16進数値しか表示されません。アセンブリコードを見るには、さらに hsdis(HotSpot DISassembler)が必要になります。hsdisは、OpenJDKプロジェクトに含まれるJavaVMのプラグインライブラリで、JITが生成した機械語からアセンブリコードに逆アセンブルします。ただし hsdisのバイナリはほとんどのOpenJDKディストリビューションには含まれていないので、次のどちらかで用意します。
- ビルド済みのhsdisライブラリを探して入手する
- OpenJDKのソースコードを入手して自分でビルドする
ビルド済みのhsdisライブラリ¶
ビルド済みのhsdisライブラリを公式に提供しているサイトはないので、野良ビルド的なものを取得することになります。このhsdisはOpenJDKのバージョンに依存する可能性があり、使用するJDKバージョンと互換性のあるバージョンのhsdisを取得します。が、互換性情報は中々見出せず、動くかどうか試してみるしかなさそうです。
次のサイトでは、本日(2026-03-03)現在、OpenJDK 17のソースコードからビルドしたhsdisライブラリを公開しています。
https://chriswhocodes.com/hsdis/
Windows OSの場合、このサイトから hsdis-amd64.dll を入手し、OpenJDK 17ディストリビューションをインストールし、OpenJDK 17のインストールディレクトリ\bin\serverの下にhsdis-amd64.dllを保存します。
C:\work> java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly MulDivShift
:
[Verified Entry Point]
# {method} {0x000001e56a4002e0} 'calc' '(I)I' in 'MulDivShift'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
0x000001e57b228b80: sub $0x18,%rsp
0x000001e57b228b87: mov %rbp,0x10(%rsp) ;*synchronization entry
; - MulDivShift::calc@-1 (line 9)
0x000001e57b228b8c: mov %edx,%eax
0x000001e57b228b8e: sar $0x1f,%eax
0x000001e57b228b91: shr $0x1e,%eax
0x000001e57b228b94: add %edx,%eax
0x000001e57b228b96: sar $0x2,%eax ;*idiv {reexecute=0 rethrow=0 return_oop=0}
; - MulDivShift::calc@2 (line 9)
0x000001e57b228b99: add $0x10,%rsp
0x000001e57b228b9d: pop %rbp
0x000001e57b228b9e: cmp 0x348(%r15),%rsp ; {poll_return}
0x000001e57b228ba5: ja 0x000001e57b228bac
0x000001e57b228bab: ret
このアセンブリコード(ニーモニック)はAT&T記法です。Intel記法で出すには、-XX:PrintAssemblyOptions=intel を指定します。
C:\work> java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:PrintAssemblyOption=intel MulDivShift
JITコンパイラは賢いので、短いメソッドは呼び出し元の箇所へインライン化したり、ある一定回数以上実行されたコードでないとJITコンパイルしないとか、第1段階では軽い最適化(C1コンパイル)で第2段階で深い最適化(C2コンパイル)するなどの振る舞いを持ちます。JITが吐き出すアセンブリコードを、インライン化させずに、C2コンパイル結果のものを Intelニーモニック形式で見るには、
次のようにオプションを指定します。
C:\work> java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:PrintAssemblyOptions=intel ^
-XX:-TieredCompilation -XX:-Inline MulDivShift
:
[Verified Entry Point]
# {method} {0x000001cd7d4002e8} 'calc' '(I)I' in 'MulDivShift'
# parm0: rdx = int
# [sp+0x20] (sp of caller)
0x000001cd5efb3280: sub rsp,0x18
0x000001cd5efb3287: mov QWORD PTR [rsp+0x10],rbp ;*synchronization entry
; - MulDivShift::calc@-1 (line 9)
0x000001cd5efb328c: mov eax,edx
0x000001cd5efb328e: sar eax,0x1f
0x000001cd5efb3291: shr eax,0x1d
0x000001cd5efb3294: add eax,edx
0x000001cd5efb3296: sar eax,0x3 ;*idiv {reexecute=0 rethrow=0 return_oop=0}
; - MulDivShift::calc@3 (line 9)
この hsdis バイナリを JDK 21および JDK 25のbin\serverの下に置いたところ、ニーモニックが表示されました。
高徹 hsdisについて補足 - 高橋 徹 さんが約2ヶ月前に追加
hsdisが逆アセンブルを行う際に利用するライブラリは、hsdisのビルド時に次の3つから選択します。
- GNU binutils
- LLVM
- Capstone
hsdisの開発当初はGNU binutilsを利用して逆アセンブルを行っていました。その後LLVMとCapstoneを利用することが可能になりました(OpenJDK 19で追加、安定してきたのは21あたりの模様)。
GNU binutilsはGPLv3のライセンスで提供され、OpenJDKのGPLv2とは互換性がありません。そのため、hsdisのバイナリを提供するとなると、ちょっと困ったことになります。これが hsdis のバイナリが大っぴらに提供されていない理由かもしれません。また、binutilsのABIがバージョンアップで不安定、libbfdが重い、Windows OS環境ではMSYS2とMinGW64を使う必要がある、などの課題もあります。
LLVMはコンパイラ基盤で、その逆アセンブラ機能(MC Disassembler API)を使います。
ビルドにはLLVMをインストールする必要がありますが、そこそこ大きなソフトウェアです。
Capstoneは、逆アセンブラ機能そのものです。
高徹 Windows OS用の hsdis をソースからビルドする方法を調査 - 高橋 徹 さんが約1ヶ月前に追加
https://github.com/openjdk/jdk/blob/master/src/utils/hsdis/README.md
2026年3月時点(OpenJDK 21/25)において、Windows OS上で動く hsdis をビルドするのは中々に難しい作業といえます。
GNU binutils¶
Visual Studio ツール系では binutils をビルドすることはできず、mingw コンパイラを使う必要がありビルド環境を整えるのに cygwin を利用することになります。
LLVM¶
Windows OS用のLLVMインストールは不完全でインクルードファイルが不足しているなど、LLVMビルド環境を構築するのに問題があると記載されています。(少なくともLLVM 13までは)
LLVMは完全なコンパイラキットではなく、コンパイルフレームワークなので、ベースに別途Cのランタイム(とstdio.hなどのインクルードファイル)が必要で、WindowsOSでは、Visual Studioツール系もしくはMingwツール系のどちらかを使用します。
Capstone¶
Windows用のCapstone Core Engineをダウンロードすると記載されていますが、現時点でCore Engineは https://www.capstone-engine.org/download.html には存在せず、Capstone Python モジュールのWindows版を入手し、その中からライブラリ(capstone.dll)を取り出すなどの回避策を取らざるを得ないようです。その上で、Visual StudioかMingwのコンパイラ、CMakeツールを使用します。
高徹 macOS用のhsdisをソースからビルドする方法を調査 - 高橋 徹 さんが30日前に追加
次のブログ記事を参考に、Capstoneを利用するhsdisをビルドします。
How to install HotSpot Disassembler (hsdis) on macOS
ビルドは、Xcode Command Line Toolsを使用します。
capstoneのインストール¶
~ % brew install capstone
OpenJDKのhsdisをクローン¶
~ % git clone --filter=blob:none --no-checkout https://github.com/openjdk/jdk.git jdk_hsdis.git
:
~ % cd jdk_hsdis.git
jdk_hsdis.git % git sparse-checkout set src/utils/hsdis
jdk_hsdis.git % git checkout
:
jdk_hsdis.git % ls
ADDITIONAL_LICENSE_INFO CONTRIBUTING.md README.md
ASSEMBLY_EXCEPTION LICENSE SECURITY.md
configure Makefile src
jdk_hsdis.git % tree src
src
└── utils
└── hsdis
├── binutils
│ └── hsdis-binutils.c
├── capstone
│ └── hsdis-capstone.c
├── hsdis-license.txt
├── hsdis.h
├── llvm
│ └── hsdis-llvm.cpp
└── README.md
ビルド¶
src/utils/hsdisには、Makefileがなくconfigureもありません。
OpenJDKはトップレベルのconfigureがサブディレクトリのMakefileを(おそらく)生成します。OpenJDK全体のconfigureは、macOSではXCode Command Line Toolsではなく、XCodeのインストールを要求します。また、トップディレクトリ直下のmakeディレクトリが必要となります。
そこで、Makefileによるビルドではなく、コンパイルコマンドを直接実行してソースファイルをコンパイルします。
jdk_hsdis.git % cd src/utils/hsdis
hsdis % clang -dynamiclib -fPIC -O2 \
-DCAPSTONE_ARCH=CS_ARCH_ARM64 \
-DCAPSTONE_MODE=CS_MODE_ARM \
hsdis-capstone.c \
-I.. -I$(brew --prefix capstone)/include/capstone \
-I$JAVA_HOME/include -I$JAVA_HOME/include/darwin \
$(brew --prefix capstone)/lib/libcapstone.a \
-o hsdis-aarch64.dylib
- hsdis-capstone.cの中で使用しているシンボル CAPSTONE_ARCHとCAPSTONE_MODE は、プラットフォームに応じたcapstoneのシンボルに定義
- hsdis.hが、hsdis-capstone.cの親ディレクトリにあるので、-I.. を指定
- capstoneのヘッダーを参照するので、-I$(brew --prefix capstone)/include/capstone を指定
capstoneは、Homebrewでインストールした場合 - JNIを使うので、JDKのincludeとinclude/darwin をインクルードパスに指定
- capstoneのライブラリを静的リンクするため、libcapstone.a を指定
- 生成ファイル名は決まっているのでそれに合わせて hsdis-aarch64.dylib とする
生成された hsdis-aarch64.dylib を、$JAVA_HOME/lib/server/ 下にコピーします。
高徹 Apple Silicon macOS用のhsdisでJITの生成するアセンブリコードを見る - 高橋 徹 さんが17日前に追加
hsdisをビルドし、JDKのディレクトリ下においたので、JITにより生成されるアセンブリコードを見てみます。
次のクラス(抜粋)で、整数を2で割るidivメソッドと浮動小数点数を2で割るfdivメソッドのアセンブリコードを調べます。
package ch2;
public class Ch2Jit {
:
int idiv(int v) {
return v / 2;
}
double fdiv(double v) {
return v / 2;
}
- 最初間違って浮動小数点数の2の冪乗除算で試してました。右1bitシフト演算が2で割った結果と等価になるのは整数のみです。
- が、怪我の功名で浮動小数点数の2の冪乗除算が乗算に最適化される結果が出たので記述を残すことにしました。
hsdisでアセンブリコードを調べる際によく見かける -XX:+PrintAssembly は、JDKのライブラリを含めて実行したすべてのクラスとメソッドを対象にしてしまうので、出力量が多く目的のメソッドを探すのが大変です。
そこで、特定のメソッドだけを出力対象とするオプションを指定します。
work % java -XX:+UnlockDiagnosticVMOptions \ "-XX:CompileCommand=print,ch2.Ch2Jit::*" \ -cp src ch2.Ch2Jit
実行結果は、
CompileCommand: print ch2/Ch2Jit.* bool print = true For seeing JIT Assembly code. ============================= C1-compiled nmethod ============================== ----------------------------------- Assembly ----------------------------------- Compiled method (c1) 17 8 3 ch2.Ch2Jit::fdiv (6 bytes) total in heap [0x0000000115234c48,0x0000000115234e70] = 552 :(中略) 0x0000000115234de0: fmov d1, #2.00000000 0x0000000115234de4: fdiv d2, d0, d1 0x0000000115234de8: fmov d0, d2 :(中略) ============================= C1-compiled nmethod ============================== ----------------------------------- Assembly ----------------------------------- Compiled method (c1) 20 9 2 ch2.Ch2Jit::idiv (4 bytes) total in heap [0x0000000111ca4e88,0x0000000111ca50b0] = 552 :(中略) 0x0000000114275220: asr w8, w2, #0x1f 0x0000000114275224: add w8, w2, w8, lsr #31 0x0000000114275228: asr w0, w8, #1 :(略)
C1コンパイル(コンパイル時間を短くすることを優先としたJITコンパイル)の結果です。
浮動小数点数の除算では次fmov d1, #2.000で、即値の2.0を浮動小数点64bitレジスタのd1に代入fdiv d2, d0, d1で、d2 = d0 / d1 の浮動小数点除算を実行fmov d0, d2で、d0 = d2と除算結果をd0レジスタに代入
asr w8, w2, #0x1fで、32bit整数レジスタのw2の値を符号付整数として右31bit算術シフトした結果をw8レジスタに代入
→ w2レジスタの値が0以上であれば結果は0、負であれば結果は-1(すべてのbitが1)add w8, w2, w8, lsr #31で、w8レジスタの値(0または-1)を右31bit論理シフトした結果(0または1)をw2に加算し結果をw8に代入
→w8 = w2 + (w8 >> 31)の意味になります。asr w0, w8, #1で、w8レジスタの値を右1bit算術シフトし結果をw0に代入
→ 整数を右1bitシフトすると2で割るのと同等の結果、ただし2で割り切れない負の整数を右1bitシフトすると、端数が負の無限大方向に丸められます。C/Javaでは負の整数を割り算したときに割り切れない時は0方向に丸める仕様なので、補正値を加える必要があります。例えば、-3 / 2 = -1ですが、-3 >> 1 = -2になってしまいます。そこで、(-3 + 1) >> 1 = -1と補正値の1を加えます。除数4のときは、補正値3を加えて2bitシフトします。
このように、C1レベルで整数を2で割るコードがシフト演算で実現される最適化コードとして生成されています。
次に、最適化が進んだC2コンパイルの結果を見るには、-XX:-TieredCompilation オプションを追加指定します。
work % java -XX:+UnlockDiagnosticVMOptions \ "-XX:CompileCommand=print,ch2.Ch2Jit::*" \ -XX:-TieredCompilation \ -cp src ch2.Ch2Jit
実行結果は、
CompileCommand: print ch2/Ch2Jit.div bool print = true For seeing JIT Assembly code. ============================= C2-compiled nmethod ============================== ----------------------------------- Assembly ----------------------------------- Compiled method (c2) 21 4 ch2.Ch2Jit::div (6 bytes) total in heap [0x0000000115d78288,0x0000000115d78418] = 400 :(中略) 0x0000000115d783bc: fmov d16, #0.50000000 0x0000000115d783c0: fmul d0, d0, d16 :(中略) ============================= C2-compiled nmethod ============================== ----------------------------------- Assembly ----------------------------------- Compiled method (c2) 18 5 ch2.Ch2Jit::idiv (4 bytes) total in heap [0x0000000115728d88,0x0000000115728f18] = 400 :(中略) 0x0000000115728ebc: add w10, w2, w2, lsr #31 0x0000000115728ec0: asr w0, w10, #1
C2コンパイル(コンパイル時間を長く要するが最適化をを優先としたJITコンパイル)の結果です。
浮動小数点数の除算では、d16レジスタに、除数の2.0の逆数0.5をJITコンパイラが計算して代入し、d0 = d0 * d16 と除算を乗算に変形したコードを生成しています。
整数の除算では、w2レジスタを右31bitシフトし符号ビットだけを残し(被除数が負のとき1、そうでなければ0)、w2レジスタと足し算してw10レジスタに入れ、w10レジスタを右1bitシフトしています。
Javaのソースコードで、return v / 4 と修正すると、fmov d16 #0.25000000 となります。
Javaのソースコードで、return v / 5 と修正すると、fdiv d0 d0 d16 と除算のままとなります。
高徹 JITWatchツールを試してみる - 高橋 徹 さんが17日前に追加
javaコマンドのオプションでメソッドのJITアセンブリコードを見るのは、ほんの短いコードでも多量のアセンブリコードが生成されるのでなかなか大変です。
JITWatchツールを使うと、GUIでJavaのコードと生成されたアセンブリコードがGUIで対比できるのでとても便利です。
JITWatchツールは、JITコンパイラのログファイルを使用するので、ターゲットコードの実行時にログを生成するオプションを追加します。
~ % java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation \ -XX:+PrintAssembly -XX:-TieredCompilation -XX:-UseCompressedOops \ -cp src ch2.Ch2Jit
- C2コンパイル結果を見たいので、
-XX:-TieredCompilation指定 - アセンブリコードを見やすくするため、compressed oopsを抑制する
-XX:-UseCompressOops指定
JITWatchツールを実行
~ % java -jar ~/Downloads/jitwatch-ui-1.5.0-shaded-macos-aarch64.jar
GUIが表示されるので、次の操作を実施
- [Config]ボタンをクリックし「JITWatch Configuration」ダイアログを表示
- Source locationsで、[Add JDK src]ボタンをクリックし、JDKのsrc.zipを指定
- Source locationsで、[Add Folder]ボタンをクリックし、ターゲットのソースファイルの基点フォルダを指定
- Class locationsで、[Add Folder]ボタンをクリックし、ターゲットのクラスファイルの基点フォルダを指定
- [Save]で設定を保存
- [Open Log]ボタンをクリックし、生成されたログファイルを指定
- [Start]ボタンをクリック
- 左ペインで、アセンブリコードを参照したいクラスを選択、右上ペインでメソッドを選択すると「TriView」画面が表示
TriView画面の表示は次のようになります。
左ペインにJavaソースコード、中ペインにJavaバイトコード、右ペインにJITが生成したアセンブリコードが表示されます。
