Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
FFMでJITコンパイラを作ってみた
Search
Yasumasa Suenaga
June 16, 2024
Technology
1.1k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
FFMでJITコンパイラを作ってみた
JJUG CCC 2024 Spring講演資料
Yasumasa Suenaga
June 16, 2024
More Decks by Yasumasa Suenaga
See All by Yasumasa Suenaga
挙動をエレガントに変えたい! ~JDK付属ツール編~
yasuenag
0
140
Other Decks in Technology
See All in Technology
スキルと MCP ツール、責務をどう分けるか? AI が迷わないインターフェース設計の戦略
cdataj
1
1.1k
200個のGitHubリポジトリを横断調査したかった
icck
0
130
日本 Fintech 未来予測レポート 2027〜2028年(手動編集版)
8maki
0
2.4k
AIはどのように 組織のアジリティを変えるのか?
junki
4
970
人材育成分科会.pdf
_awache
4
270
SONiCのLinuxベースを活かしたZabbix監視
sonic
0
190
Oracle AI Database@Azure:サービス概要のご紹介
oracle4engineer
PRO
6
2k
日本 Fintech 未来予測レポート 2027〜2028年(オリジナル版)
8maki
0
2.3k
Oracle AI Database@Google Cloud:サービス概要のご紹介
oracle4engineer
PRO
6
1.5k
MUSUBI 田中裕一『AIと共に行う「しごとのリデザイン」- スモールバックオフィス編』AI Ops Lab #4
musubi
0
210
プロダクト開発から業務改善コンサルまで。事業全体へ「染み出す」ことで広がるエンジニアの可能性
ham0215
0
130
SONiC Scale-Up Working Group から探る Scale-UpやUltraEthernet機能の実装方法
ebiken
PRO
2
360
Featured
See All Featured
VelocityConf: Rendering Performance Case Studies
addyosmani
333
25k
Navigating Weather and Climate Data
rabernat
0
220
SEOcharity - Dark patterns in SEO and UX: How to avoid them and build a more ethical web
sarafernandez
0
200
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
330
A Soul's Torment
seathinner
6
2.9k
Have SEOs Ruined the Internet? - User Awareness of SEO in 2025
akashhashmi
0
370
Reality Check: Gamification 10 Years Later
codingconduct
0
2.2k
Collaborative Software Design: How to facilitate domain modelling decisions
baasie
1
250
The Illustrated Guide to Node.js - THAT Conference 2024
reverentgeek
1
390
Test your architecture with Archunit
thirion
1
2.3k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
47
8.2k
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
Transcript
#jjug_ccc #jjug_ccc_a1 FFMで JITコンパイラを作ってみた 末永 恭正 @YaSuenag
#jjug_ccc #jjug_ccc_a1 似た話 Panamaを先取り!? JVMCIでJITと遊ぶ • JVM Compiler Interface •
JavaでJITを作るための インターフェース • Graal Compilerでも利用 • Java Day Tokyo 2017で 講演しました
#jjug_ccc #jjug_ccc_a1 Foreign Function & Memory API • Javaの外の関数とメモリを扱うためのAPI •
Project Panamaの成果物を流用している JDK 14 JDK 16 JDK 17 JDK 19 JDK 22 Foreign Memory Foreign Linker Foreign Function & Memory 最初のJEPが登場したJDK 14(2020/03)から4年かけて完成!
#jjug_ccc #jjug_ccc_a1 一般的な “関数呼び出し” を分解してみる メモリ ① ライブラリ ① メモリにライブラリをロードする
② 目的の関数がある場所を見つける ③ 見つけた場所を呼び出す ② ③
#jjug_ccc #jjug_ccc_a1 FFMによる外部関数の呼び出し メモリ ① ライブラリ ② ③ ① メモリにライブラリをロードする→SymbolLookupを得る
② 目的の関数がある場所を見つける→MemorySegmentを得る ③ 見つけた場所を呼び出す→MethodHandleをinvokeする
#jjug_ccc #jjug_ccc_a1 FFMでやっていることは Cでやっていることと一緒!
#jjug_ccc #jjug_ccc_a1 Just In Timeコンパイラ • 実行時にプログラムを機械語へコンパイルする機能 • JavaのHotSpotや.NETのRyuJIT、JSのV8など 様々な言語ランタイムで導入されている
• HotSpotの場合 • “Hot”と判断されたメソッドが実行状況を加味して 最適化されたうえで機械語へコンパイルされる • 最適化のレベルが細分化されている(Tiered Compilation) • 配列コピーやMD5ハッシュ値計算などの「処理」のほか 例外キャッチや引数処理など内部処理効率化のコード生成も行う
#jjug_ccc #jjug_ccc_a1 Just In Timeコンパイラ • 実行時にプログラムを機械語へコンパイルする機能 • JavaのHotSpotや.NETのRyuJIT、JSのV8など 様々な言語ランタイムで導入されている
• HotSpotの場合 • “Hot”と判断されたメソッドが実行状況を加味して 最適化されたうえで機械語へコンパイルされる • 最適化のレベルが細分化されている(Tiered Compilation) • 配列コピーやMD5ハッシュ値計算などの「処理」のほか 例外キャッチや引数処理など内部処理効率化のコード生成も行う 本日の中心
#jjug_ccc #jjug_ccc_a1 “JIT生成コードが動く” を分解してみる メモリ ① 実行可能メモリ ① 実行可能メモリをアロケートする ②
機械語を生成し、実行可能メモリに書き込む ③ 書き込んだ機械語の先頭アドレスを呼び出す ② ③ ライブラリの ロードと等価
#jjug_ccc #jjug_ccc_a1 • 普通の外部関数呼び出しと大筋は変わらなさそう • がんばれば ”なんちゃって” JITができそう FFMで作ってみます!
#jjug_ccc #jjug_ccc_a1 おことわり • 動作確認は以下の環境で行っています • CPU:AMD Ryzen 3 3300X
• OS:Fedora 40 AMD64 • Windows 11(23H2)のHyper-V上で動作、4vCPU、メモリ8GB • カーネル:6.8.7-300.fc40.x86_64 • glibc:2.39-8.fc40.x86_64 • Java:Oracle OpenJDK 22.0.1 Linux x64 • 各種ソースコードは以下のものです • OpenJDK:リビジョン c3372c4 • ffmasm:リビジョン 9d27da1 • 上記以外のソースコード、スクリーンショット等は動作確認環境で取得したものです • ブラウザスクリーンショットは2024/6/15現在のものです
#jjug_ccc #jjug_ccc_a1 本日の参考資料 YaSuenag/ffmasm: hand-assembler for Java
#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する ③ 作った機械語を呼び出す FFM版JIT実装への3ステップ
#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する ③ 作った機械語を呼び出す
#jjug_ccc #jjug_ccc_a1 実行可能メモリ • 実行可能フラグが立ったメモリ空間 • Linuxだと “x” が立っている領域 •
普通にmalloc()しただけでは実行可能にならない • ”x”が立たない • HotSpotのコードキャッシュは 実行可能メモリとして確保されている CodeHeap 'profiled nmethods': size=120028Kb used=4302Kb max_used=4302Kb free=115725Kb bounds [0x00007f6694cc8000, 0x00007f6695108000, 0x00007f669c1ff000] 7f6694cc8000-7f6695108000 rwxp 00000000 00:00 0
#jjug_ccc #jjug_ccc_a1 課題:実行可能なMemorySegmentは作れない Arenaの各ライフサイクルでアロケートできるMemorySegmentは 実行権限を持たない Arenaメソッド global() ofConfined() ofShared() ofAuto()
MemorySessionImpl実装 GlobalSession ConfinedSession SharedSession ImplicitSession ArenaImpl allocateNoInit() SegmentFactories allocateSegment() Unsafe allocateMemory() allocate() JNI
#jjug_ccc #jjug_ccc_a1 Unsafe::allocateMemoryのJNI実装 src/hotspot/share/prims/unsafe.cpp src/hotspot/share/runtime/os.cpp ※os::malloc()の抜粋 src/hotspot/share/utilities/compilerWarnings_gcc.hpp src/hotspot/share/utilities/compilerWarnings_visCPP.hpp
#jjug_ccc #jjug_ccc_a1 どうやって実行可能メモリを作るか? • OSが提供するAPIをFFMで直接呼び出す • HotSpotがコードキャッシュを作る方法と同じ Linuxの場合:mmap() Windowsの場合:VirtualAlloc()
#jjug_ccc #jjug_ccc_a1 Linuxの場合 mmap()をPROT_EXECつきで呼び出す ffmasm/src/main/java/com/yasuenag/ffmasm/internal/linux/LinuxExecMemory.java
#jjug_ccc #jjug_ccc_a1 定数の定義場所 /usr/include/bits/mman-linux.h ※Fedoraの場合
#jjug_ccc #jjug_ccc_a1 Windowsの場合 VirtualAlloc()をPAGE_EXECUTE_READWRITEつきで呼び出す ffmasm/src/main/java/com/yasuenag/ffmasm/internal/windows/WindowsExecMemory.java
#jjug_ccc #jjug_ccc_a1 定数の定義場所 VirtualAlloc 関数
#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する ③ 作った機械語を呼び出す
#jjug_ccc #jjug_ccc_a1 機械語 - Wikipedia
#jjug_ccc #jjug_ccc_a1 ただ (int)1 を返すだけのC言語関数 Linux AMD64の場合 機械語
#jjug_ccc #jjug_ccc_a1 意識しなければならないこと • 呼び出し規約 • 関数の呼び出しと戻るときの “お約束” • 処理系に強く依存
• 命令のエンコーディング • アセンブラコードを、どのように0と1で表すか? • CPUによって異なる • OSは関係なし
#jjug_ccc #jjug_ccc_a1 呼び出し規約 • レジスタの使い方 • 呼び出された直後にやるべきこと→プロローグ • 呼び出し元に戻る前にやるべきこと→エピローグ
#jjug_ccc #jjug_ccc_a1 レジスタの使い方 • 各レジスタの意味は処理系によって意味が異なる • 引数に使うもの、保持しておかなければならないもの、etc… Linux:System V ABI
(Linux Foundation Referenced Specifications 内) Windows:Microsoft Learn (x64 ABI規則の概要 内)
#jjug_ccc #jjug_ccc_a1 プロローグ • 関数に入った直後の “お約束” • レジスタ退避や自動変数用の領域確保のほか スタックフレーム作成を行う場合もある
#jjug_ccc #jjug_ccc_a1 エピローグ • 呼び出し元に戻るときの “お約束” • 保持すべきレジスタの復帰のほかスタックポインタを復元 してから呼び出し元に戻る
#jjug_ccc #jjug_ccc_a1 命令のエンコーディング:Intel 64の場合 • Intel® 64 and IA-32 Architectures
Software Developer's Manual • 略して “SDM” • 自分が動かしたいIntel 64のアセンブラコードを どう機械語に落とし込めばいいかのすべてが書かれている • 単純な命令であってもプリフィックスとModR/Mは要注意
#jjug_ccc #jjug_ccc_a1 Javaで機械語を作るには a. 動かしたいアセンブラコードをイメージする b. 命令をハンドアセンブルする c. 実行可能メモリへ書き込む
#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する a. 動かしたいアセンブラコードをイメージする b. 命令をハンドアセンブルする
c. 実行可能メモリへ書き込む ③ 作った機械語を呼び出す
#jjug_ccc #jjug_ccc_a1 例:単純な関数 2つの引数を足して返す
#jjug_ccc #jjug_ccc_a1 アセンブラコード全体像 プロローグ 処理本体 エピローグ ※前ページのCコードをコンパイルしても、これに対応する機械語が生成されるとは限りません
#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する a. 動かしたいアセンブラコードをイメージする b. 命令をハンドアセンブルする
c. 実行可能メモリへ書き込む ③ 作った機械語を呼び出す
#jjug_ccc #jjug_ccc_a1 バイナリで見ると…
#jjug_ccc #jjug_ccc_a1 MOV命令の場合 01001000 10001001 11100101 REXプリフィックス オペコード ModR/M
#jjug_ccc #jjug_ccc_a1 REXプリフィックス 0100 1 0 0 0 固定値 W
R X B
#jjug_ccc #jjug_ccc_a1 オペコード
#jjug_ccc #jjug_ccc_a1 ModR/M 11 100 101 モード (Mod) reg r/m
#jjug_ccc #jjug_ccc_a1 ModR/M reg r/m Mod SDMの「早見表」が 便利です!
#jjug_ccc #jjug_ccc_a1 ModR/Mの注意点 A) オペランドが2つでない場合、決まった固定値が入る B) R8以降のGPRを使う場合 REX.R(reg)やREX.B(r/m)を 設定する必要がある 11
010 000 モード (Mod) reg (固定値 (/2)) r/m (この場合RAX)
#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する a. 動かしたいアセンブラコードをイメージする b. 命令をハンドアセンブルする
c. 実行可能メモリへ書き込む ③ 作った機械語を呼び出す
#jjug_ccc #jjug_ccc_a1 書き込み 直接MemorySegmentに書き込んでもよいですが… ByteBuffer経由の書き込みがオススメ! • 理由:ラクだから • 様々なデータ型をput()で書き込める •
開始アドレスさえ決まればインデックスを意識しなくていい • 注意点 • マルチバイトなデータはネイティブバイトオーダーで書き込む必要がある
#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する ③ 作った機械語を呼び出す
#jjug_ccc #jjug_ccc_a1 生成したコードの実行 例:ffmasmでRDTSCを呼び出す ffmasm/benchmarks/funccall
#jjug_ccc #jjug_ccc_a1 critical?
#jjug_ccc #jjug_ccc_a1 critical()がコード実行に与える影響
#jjug_ccc #jjug_ccc_a1 Linker#critical(boolean) Linker.Option (Java SE 22 & JDK 22)
(oracle.com) Critical Functionとは: • どのような状況でも 実行時間がものすごく短い • upcallしない パフォーマンスゲインを 得やすいが… 一歩間違うと パフォーマンス劣化や JVMのクラッシュを 引き起こす可能性大
#jjug_ccc #jjug_ccc_a1 criticalがGCとバッティングすると… sleep(3)をcriticalで呼び出した裏で、定期的にGCを回す
#jjug_ccc #jjug_ccc_a1 criticalがGCとバッティングすると… TID 2238が止められずタイムアウト発生(-XX:+SafepointTimeout) sleep(3)実行中なのに状態が_thread_in_javaで、コールスタックも見えない ※スレッドダンプはjhsdb jstackで確認 $ java
--enable-native-access=ALL-UNNAMED -XX:+SafepointTimeout Hang [11.014s][warning][safepoint] [11.014s][warning][safepoint] # SafepointSynchronize::begin: Timeout detected: [11.014s][warning][safepoint] # SafepointSynchronize::begin: Timed out while spinning to reach a safepoint. [11.014s][warning][safepoint] # SafepointSynchronize::begin: Threads which did not reach the safepoint: [11.014s][warning][safepoint] # "main" #1 [2238] prio=5 os_prio=0 cpu=108.89ms elapsed=11.01s tid=0x00007f0428028490 nid=2238 runnable [0x0000000000000000] [11.014s][warning][safepoint] java.lang.Thread.State: RUNNABLE [11.014s][warning][safepoint] [11.014s][warning][safepoint] # SafepointSynchronize::begin: (End of list)
#jjug_ccc #jjug_ccc_a1 わざとクラッシュさせて、コアからコールスタックを見てみます Linuxでabort()を呼びます critical()の作用する場所
#jjug_ccc #jjug_ccc_a1 ----------------- 2359 ----------------- "main" #1 prio=5 tid=0x00007f63b0028340 nid=2359
runnable [0x00007f63b73bc000] java.lang.Thread.State: RUNNABLE JavaThread state: _thread_in_native 0x00007f63b7540144 __pthread_kill_implementation + 0x114 0x00007f63b74e865e __GI_raise + 0x1e 0x00007f63b74d0902 __GI_abort + 0xdf 0x00007f63a02773b7 <RuntimeStub> 0x00007f63a013f1e0 * java.lang.invoke.LambdaForm$MH+0x00007f6337019800.invoke(java.lang.Object, long) bci:7 (Interpreted frame) 0x00007f63a013f1e0 * java.lang.invoke.LambdaForm$MH+0x00007f633701cc00.invokeExact_MT(java.lang.Object, long, java.lang.Object) bci:19 (Interpreted frame) 0x00007f63a013f1e0 * jdk.internal.foreign.abi.DowncallStub+0x00007f633701a800.invoke(java.lang.foreign.SegmentAllocator, java.lang.foreign.MemorySegment) bci:39 (Interpreted frame) 0x00007f63a013f1e0 * java.lang.invoke.LambdaForm$DMH+0x00007f633701ac00.invokeStaticInit(java.lang.Object, java.lang.Object, java.lang.Object) bci:11 (Interpreted frame) 0x00007f63a013f1e0 * java.lang.invoke.LambdaForm$MH+0x00007f633701bc00.invoke(java.lang.Object) bci:41 (Interpreted frame) 0x00007f63a013f1e0 * java.lang.invoke.LambdaForm$MH+0x00007f633701c000.invoke_MT(java.lang.Object, java.lang.Object) bci:17 (Interpreted frame) 0x00007f63a013f1e0 * Abort.main(java.lang.String[]) bci:87 line:13 (Interpreted frame) critical()の作用する場所 jhsdb jstack --mixedの出力 ココ(Javaと呼び出し関数の橋渡し部分)
#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~スタブコード~ スタブコードが全然違う critical non-critical
#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~スタブコード~ criticalで省略されるもの:Thread Stateの変更 • _thread_in_java
⇔ _thread_in_native • 制御レジスタなどの復元 • メモリバリアの設定 • Safepointのポーリング
#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~パフォーマンス~ • RDTSCを呼び出し結果を1つの64ビット値にまとめて返すだけの 実行パフォーマンスをJMHで測定する •
FFMは同じMemorySegmentをcriticalとnon-criticalのMethodHandleを 作成して呼び出す ffmasm/benchmarks/funccall
#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~パフォーマンス~
#jjug_ccc #jjug_ccc_a1 ヒープメモリアクセス critical()利用の大きなメリット
#jjug_ccc #jjug_ccc_a1 ヒープメモリアクセス • critical()の引数にtrueを与えると、呼び出した関数から 直接Javaヒープ上のアドレスを参照できる • MemorySegmentを作ってコピーして処理して 最後にJavaヒープに戻す、ということをやらなくて済む •
ただし、MethodHandleに渡すのはMemorySegmentになるため パラメータはMemorySegment.ofArray()で作る必要がある
#jjug_ccc #jjug_ccc_a1 sprintf()にcritical(true)で配列を渡してみる 参考: Serviceability Toolsの裏側 garbage-first heap total reserved
2033664K, committed 131072K, used 4416K [0x0000000083e00000, 0x0000000100000000) region size 1024K, 4 young (4096K), 0 survivors (0K) Metaspace used 1628K, committed 1792K, reserved 1114112K class space used 171K, committed 256K, reserved 1048576K sprintf: 0x8b9e92a8 Javaヒープを直接触れる =領域外アクセスでクラッシュの可能性高! sprintf()でbyte[]のアドレスを書き込み、それをJavaのStringにする
#jjug_ccc #jjug_ccc_a1 配列オブジェクトを破壊してみる Panamaを先取り!? JVMCIでJITと遊ぶ Javaの配列は中身の直前に (C++の)int で 長さが入っている
#jjug_ccc #jjug_ccc_a1 配列オブジェクトを破壊してみる Java配列アドレス (MemorySegment)-4 に位置する “長さ” をゼロに上書きする new int[100]
したのに 関数コール後に array.length が ゼロになっている!
#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~SIMDパフォーマンス~ • 8要素の固定長int配列の足し算の実行パフォーマンスをJMHで測定 • 同じMemorySegmentをcriticalとnon-criticalのMethodHandleを
作成して呼び出す • critical()の引数はtrueに設定する ffmasm/benchmarks/vectorapi
#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~SIMDパフォーマンス~ ※素のJava呼び出しやVector APIとの比較は、ぜひご自身で確認を!
#jjug_ccc #jjug_ccc_a1 FFMでいろいろできるのはわかったけど MethodHandleが分かりにくい JNIみたいにできれば 可読性も上がるのに…
#jjug_ccc #jjug_ccc_a1 FFM生成コードを JNI関数としてバインドする
#jjug_ccc #jjug_ccc_a1 JNI関数が動くまで ライブラリをロードする(System.loadLibrary()など) JNI関数を実行しようとする ライブラリからJNI関数を見つける(初回実行時のみ) 関数ポインタをnativeメソッドと紐づける(初回実行時のみ) 実行する 初回実行時にnativeメソッドと関数ポインタをバインドする …
#jjug_ccc #jjug_ccc_a1 もう1つの方法:JNI関数の動的バインド • JNI関数のRegisterNatives()を使えば 動的に関数ポインタをnativeメソッドと 紐づけることができる • クラスライブラリの実装でも よくやっている手法
• 登録には以下の情報が必要 • 登録するクラスのjclass • 登録するメソッドの名前 • 登録するメソッドのシグネチャ • 紐づける関数ポインタ Java Native Interface Specification: 4 - JNI Functions (oracle.com)
#jjug_ccc #jjug_ccc_a1 動的バインド ライブラリをロードするなりメモリ上に展開するなりする RegisterNatives()でnativeメソッドに関数ポインタを紐づける RegisterNatives()でnativeメソッドを関数ポインタとバインドする
#jjug_ccc #jjug_ccc_a1 jclass • JNIでClassを表現する型 • FindClass()で取得可能 Java Native Interface
Specification: 4 - JNI Functions (oracle.com)
#jjug_ccc #jjug_ccc_a1 FindClass()が使えない!? FindClass locates the class loader associated with
the current native method; that is, the class loader of the class that declared the native method. JNIのFindClass()定義より: FindClass()を呼び出したネイティブメソッドに紐づいた クラスローダを検出する
#jjug_ccc #jjug_ccc_a1 FindClass()が使えない!? src/hotspot/share/prims/jni.cpp 自分のクラスローダに紐づいた クラスしか探索できないため 汎用性を持たせられない!
#jjug_ccc #jjug_ccc_a1 オレたちにはJVMTIがあるじゃないか!
#jjug_ccc #jjug_ccc_a1 Java Virtual Machine Tool Interface • Javaのデバッグや監視に便利な インターフェース(API)
• ネイティブな共有ライブラリ形式 • java起動引数でアタッチする • jcmdで動的にアタッチする • 主な機能 • JVM内部イベントのフック • クラス書き換え • オブジェクトの参照関係追跡 JVM(TM) Tool Interface 22.0.0 (oracle.com)
#jjug_ccc #jjug_ccc_a1 なぜJVMTIなのか クラスローダ関係なく JVM中に存在するクラスを すべて取得してくれる! JVM(TM) Tool Interface 22.0.0
(oracle.com) GetLoadedClasses()があるから
#jjug_ccc #jjug_ccc_a1 JNIをバインドするまで GetLoadedClasses()で、ロード済み全jclassを取得する 目的のjclassを見つける RegisterNatives()でJNI関数を登録する FFM FFM Java
#jjug_ccc #jjug_ccc_a1 高い壁:JNI参照 GetLoadedClasses()で返されるjclassはJNIのローカル参照! 一度ネイティブ呼び出しから抜けるとjclassが無効になってしまう! src/hotspot/share/prims/jvmtiGetLoadedClasses.cpp src/hotspot/share/prims/jvmtiEnvBase.cpp
#jjug_ccc #jjug_ccc_a1 JNIをバインドするまで せっかくのjclassが無効になってしまう! 1度Javaの世界を経由する GetLoadedClasses()で、ロード済み全jclassを取得する 目的のjclassを見つける RegisterNatives()でJNI関数を登録する FFM FFM
Java
#jjug_ccc #jjug_ccc_a1 JNIをバインドするまで GetLoadedClasses()で、ロード済み全jclassを取得する 目的のjclassを見つけ、RegisterNatives()呼び出し準備をする RegisterNatives()でJNI関数を登録する 全部を一息に終わらせる処理をFFMで作り Javaでやりたい部分はupcallで呼び出す! FFM upcall
#jjug_ccc #jjug_ccc_a1 JavaVMとJNIEnvとjvmtiEnv • JNI関数、JVMTI関数の実行には それぞれJNIEnv、jvmtiEnvが 必要 • C++の クラスインスタンスのようなもの
• thisポインタのように 必ず 引数に与えなければならない • これらのインスタンスは JavaVMインスタンスから取得する JavaVM JNIEnv jvmtiEnv GetEnv(JNI version) GetEnv(JVMTI version)
#jjug_ccc #jjug_ccc_a1 JavaVMとJNIEnvとjvmtiEnv • これらのインスタンスに含まれる関数には すべてインデックス番号がついている • 実体はCの構造体のため そこの関数テーブルの インデックスを表している
• これらを地道に解決して 呼び出していく…
#jjug_ccc #jjug_ccc_a1 あまりにも大変&地道に呼ぶだけなので… ffmasmの実装を見てください! ffmasm/src/main/java/com/yasuenag/ffmasm/internal • JavaVM.java • JniEnv.java •
JvmtiEnv.java 注目ポイント: 関数テーブルを探す→呼び出す ※繰り返し
#jjug_ccc #jjug_ccc_a1 “一息” の全体像 ffmasm/src/main/java/com/yasuenag/ffmasm/internal/amd64/AMD64NativeRegister.java GetLoadedClass() upcall jclass探索と RegisterNatives() GetLoadedClass()で
確保したメモリの クリーンアップ
#jjug_ccc #jjug_ccc_a1 Shadow Space • デバッグ目的など、Windowsで レジスタ渡しの4つの引数を 格納するためのスタック領域 • 64bitレジスタ×4
= 32バイトが 必要 • 呼び出し直前にRSPが16バイトで アラインされている必要がある x64 でのスタックの使用 | Microsoft Learn これが守られていないと call命令発行時にJVMがクラッシュする!
#jjug_ccc #jjug_ccc_a1 upcallの内容 ffmasm/src/main/java/com/yasuenag/ffmasm/NativeRegister.java クラス探し (ループ) クラス名比較 (strcmp()) RegisterNatives()
#jjug_ccc #jjug_ccc_a1 JNI関数として登録する場合の注意点 • 第1引数は必ずJNIEnvへのポインタ • 第2引数は何らかのインスタンス • インスタンスメソッドの場合:thisインスタンス •
staticメソッドの場合:classインスタンス JNI関数をバインドする場合は 実質第3引数からしか使えない!
#jjug_ccc #jjug_ccc_a1 パフォーマンス • 先ほどご紹介したRDTSCの例で計測 • JNI関数はそこに入る時点で_thread_in_nativeへの ステート遷移が発生するので、critical関数として登録 素のJNI呼び出しとFFMのJNIバインドは スコアがほぼ変わらない!
#jjug_ccc #jjug_ccc_a1 MethodHandleの定義次第では… criticalが遅い… ffmasm リビジョン fff651f でのベンチ結果
#jjug_ccc #jjug_ccc_a1 JMH実行時のJNIとFFMのコールパス JMH実行時に -prof async でFlameGraphを見てみると… JNI呼び出し FFM呼び出し ※ffmasm
リビジョン fff651f でのベンチ結果
#jjug_ccc #jjug_ccc_a1 static finalか否かでのインライン化の違い 修正後:MethodHandleがstatic final 修正前:MethodHandleが普通のクラスメンバ インライン化される対象が 全然違う!
#jjug_ccc #jjug_ccc_a1 static finalのときのみに存在するログ ネイティブ呼び出し部分まで インライン化される!
#jjug_ccc #jjug_ccc_a1 HotSpotから見たstatic finalフィールドの扱い 定数フィールドとして 扱われるため より最適化が効きやすくなる! src/hotspot/share/ci/ciField.cpp
#jjug_ccc #jjug_ccc_a1 まとめ
#jjug_ccc #jjug_ccc_a1 まとめ • FFMを使えばJavaだけで動的に機械語を生成して実行可能 • ただし、いくつか考慮は必要 • 実行用メモリ空間 •
呼び出し規約 • 命令エンコーディング • critical()使用時のJVMに与える影響 • JNI/JVMTI関数呼び出し • critical()で関数の呼び出しコストを下げられる • ただし、MethodHandleの定義の仕方やJITのかかり方に注意
#jjug_ccc #jjug_ccc_a1 まとめ ※大事なこと 気合と根性で何とかなる!
#jjug_ccc #jjug_ccc_a1 最後に • ffmasmの協力者募集中です! • さらなる命令拡充 • Arm64対応 •
バグフィックス • ドキュメント • …
#jjug_ccc #jjug_ccc_a1