Slide 1

Slide 1 text

#jjug_ccc #jjug_ccc_a1 FFMで JITコンパイラを作ってみた 末永 恭正 @YaSuenag

Slide 2

Slide 2 text

#jjug_ccc #jjug_ccc_a1 似た話 Panamaを先取り!? JVMCIでJITと遊ぶ • JVM Compiler Interface • JavaでJITを作るための インターフェース • Graal Compilerでも利用 • Java Day Tokyo 2017で 講演しました

Slide 3

Slide 3 text

#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年かけて完成!

Slide 4

Slide 4 text

#jjug_ccc #jjug_ccc_a1 一般的な “関数呼び出し” を分解してみる メモリ ① ライブラリ ① メモリにライブラリをロードする ② 目的の関数がある場所を見つける ③ 見つけた場所を呼び出す ② ③

Slide 5

Slide 5 text

#jjug_ccc #jjug_ccc_a1 FFMによる外部関数の呼び出し メモリ ① ライブラリ ② ③ ① メモリにライブラリをロードする→SymbolLookupを得る ② 目的の関数がある場所を見つける→MemorySegmentを得る ③ 見つけた場所を呼び出す→MethodHandleをinvokeする

Slide 6

Slide 6 text

#jjug_ccc #jjug_ccc_a1 FFMでやっていることは Cでやっていることと一緒!

Slide 7

Slide 7 text

#jjug_ccc #jjug_ccc_a1 Just In Timeコンパイラ • 実行時にプログラムを機械語へコンパイルする機能 • JavaのHotSpotや.NETのRyuJIT、JSのV8など 様々な言語ランタイムで導入されている • HotSpotの場合 • “Hot”と判断されたメソッドが実行状況を加味して 最適化されたうえで機械語へコンパイルされる • 最適化のレベルが細分化されている(Tiered Compilation) • 配列コピーやMD5ハッシュ値計算などの「処理」のほか 例外キャッチや引数処理など内部処理効率化のコード生成も行う

Slide 8

Slide 8 text

#jjug_ccc #jjug_ccc_a1 Just In Timeコンパイラ • 実行時にプログラムを機械語へコンパイルする機能 • JavaのHotSpotや.NETのRyuJIT、JSのV8など 様々な言語ランタイムで導入されている • HotSpotの場合 • “Hot”と判断されたメソッドが実行状況を加味して 最適化されたうえで機械語へコンパイルされる • 最適化のレベルが細分化されている(Tiered Compilation) • 配列コピーやMD5ハッシュ値計算などの「処理」のほか 例外キャッチや引数処理など内部処理効率化のコード生成も行う 本日の中心

Slide 9

Slide 9 text

#jjug_ccc #jjug_ccc_a1 “JIT生成コードが動く” を分解してみる メモリ ① 実行可能メモリ ① 実行可能メモリをアロケートする ② 機械語を生成し、実行可能メモリに書き込む ③ 書き込んだ機械語の先頭アドレスを呼び出す ② ③ ライブラリの ロードと等価

Slide 10

Slide 10 text

#jjug_ccc #jjug_ccc_a1 • 普通の外部関数呼び出しと大筋は変わらなさそう • がんばれば ”なんちゃって” JITができそう FFMで作ってみます!

Slide 11

Slide 11 text

#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現在のものです

Slide 12

Slide 12 text

#jjug_ccc #jjug_ccc_a1 本日の参考資料 YaSuenag/ffmasm: hand-assembler for Java

Slide 13

Slide 13 text

#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する ③ 作った機械語を呼び出す FFM版JIT実装への3ステップ

Slide 14

Slide 14 text

#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する ③ 作った機械語を呼び出す

Slide 15

Slide 15 text

#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

Slide 16

Slide 16 text

#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

Slide 17

Slide 17 text

#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

Slide 18

Slide 18 text

#jjug_ccc #jjug_ccc_a1 どうやって実行可能メモリを作るか? • OSが提供するAPIをFFMで直接呼び出す • HotSpotがコードキャッシュを作る方法と同じ Linuxの場合:mmap() Windowsの場合:VirtualAlloc()

Slide 19

Slide 19 text

#jjug_ccc #jjug_ccc_a1 Linuxの場合 mmap()をPROT_EXECつきで呼び出す ffmasm/src/main/java/com/yasuenag/ffmasm/internal/linux/LinuxExecMemory.java

Slide 20

Slide 20 text

#jjug_ccc #jjug_ccc_a1 定数の定義場所 /usr/include/bits/mman-linux.h ※Fedoraの場合

Slide 21

Slide 21 text

#jjug_ccc #jjug_ccc_a1 Windowsの場合 VirtualAlloc()をPAGE_EXECUTE_READWRITEつきで呼び出す ffmasm/src/main/java/com/yasuenag/ffmasm/internal/windows/WindowsExecMemory.java

Slide 22

Slide 22 text

#jjug_ccc #jjug_ccc_a1 定数の定義場所 VirtualAlloc 関数

Slide 23

Slide 23 text

#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する ③ 作った機械語を呼び出す

Slide 24

Slide 24 text

#jjug_ccc #jjug_ccc_a1 機械語 - Wikipedia

Slide 25

Slide 25 text

#jjug_ccc #jjug_ccc_a1 ただ (int)1 を返すだけのC言語関数 Linux AMD64の場合 機械語

Slide 26

Slide 26 text

#jjug_ccc #jjug_ccc_a1 意識しなければならないこと • 呼び出し規約 • 関数の呼び出しと戻るときの “お約束” • 処理系に強く依存 • 命令のエンコーディング • アセンブラコードを、どのように0と1で表すか? • CPUによって異なる • OSは関係なし

Slide 27

Slide 27 text

#jjug_ccc #jjug_ccc_a1 呼び出し規約 • レジスタの使い方 • 呼び出された直後にやるべきこと→プロローグ • 呼び出し元に戻る前にやるべきこと→エピローグ

Slide 28

Slide 28 text

#jjug_ccc #jjug_ccc_a1 レジスタの使い方 • 各レジスタの意味は処理系によって意味が異なる • 引数に使うもの、保持しておかなければならないもの、etc… Linux:System V ABI (Linux Foundation Referenced Specifications 内) Windows:Microsoft Learn (x64 ABI規則の概要 内)

Slide 29

Slide 29 text

#jjug_ccc #jjug_ccc_a1 プロローグ • 関数に入った直後の “お約束” • レジスタ退避や自動変数用の領域確保のほか スタックフレーム作成を行う場合もある

Slide 30

Slide 30 text

#jjug_ccc #jjug_ccc_a1 エピローグ • 呼び出し元に戻るときの “お約束” • 保持すべきレジスタの復帰のほかスタックポインタを復元 してから呼び出し元に戻る

Slide 31

Slide 31 text

#jjug_ccc #jjug_ccc_a1 命令のエンコーディング:Intel 64の場合 • Intel® 64 and IA-32 Architectures Software Developer's Manual • 略して “SDM” • 自分が動かしたいIntel 64のアセンブラコードを どう機械語に落とし込めばいいかのすべてが書かれている • 単純な命令であってもプリフィックスとModR/Mは要注意

Slide 32

Slide 32 text

#jjug_ccc #jjug_ccc_a1 Javaで機械語を作るには a. 動かしたいアセンブラコードをイメージする b. 命令をハンドアセンブルする c. 実行可能メモリへ書き込む

Slide 33

Slide 33 text

#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する a. 動かしたいアセンブラコードをイメージする b. 命令をハンドアセンブルする c. 実行可能メモリへ書き込む ③ 作った機械語を呼び出す

Slide 34

Slide 34 text

#jjug_ccc #jjug_ccc_a1 例:単純な関数 2つの引数を足して返す

Slide 35

Slide 35 text

#jjug_ccc #jjug_ccc_a1 アセンブラコード全体像 プロローグ 処理本体 エピローグ ※前ページのCコードをコンパイルしても、これに対応する機械語が生成されるとは限りません

Slide 36

Slide 36 text

#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する a. 動かしたいアセンブラコードをイメージする b. 命令をハンドアセンブルする c. 実行可能メモリへ書き込む ③ 作った機械語を呼び出す

Slide 37

Slide 37 text

#jjug_ccc #jjug_ccc_a1 バイナリで見ると…

Slide 38

Slide 38 text

#jjug_ccc #jjug_ccc_a1 MOV命令の場合 01001000 10001001 11100101 REXプリフィックス オペコード ModR/M

Slide 39

Slide 39 text

#jjug_ccc #jjug_ccc_a1 REXプリフィックス 0100 1 0 0 0 固定値 W R X B

Slide 40

Slide 40 text

#jjug_ccc #jjug_ccc_a1 オペコード

Slide 41

Slide 41 text

#jjug_ccc #jjug_ccc_a1 ModR/M 11 100 101 モード (Mod) reg r/m

Slide 42

Slide 42 text

#jjug_ccc #jjug_ccc_a1 ModR/M reg r/m Mod SDMの「早見表」が 便利です!

Slide 43

Slide 43 text

#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)

Slide 44

Slide 44 text

#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する a. 動かしたいアセンブラコードをイメージする b. 命令をハンドアセンブルする c. 実行可能メモリへ書き込む ③ 作った機械語を呼び出す

Slide 45

Slide 45 text

#jjug_ccc #jjug_ccc_a1 書き込み 直接MemorySegmentに書き込んでもよいですが… ByteBuffer経由の書き込みがオススメ! • 理由:ラクだから • 様々なデータ型をput()で書き込める • 開始アドレスさえ決まればインデックスを意識しなくていい • 注意点 • マルチバイトなデータはネイティブバイトオーダーで書き込む必要がある

Slide 46

Slide 46 text

#jjug_ccc #jjug_ccc_a1 ① 実行可能メモリを作成する ② 機械語を作成する ③ 作った機械語を呼び出す

Slide 47

Slide 47 text

#jjug_ccc #jjug_ccc_a1 生成したコードの実行 例:ffmasmでRDTSCを呼び出す ffmasm/benchmarks/funccall

Slide 48

Slide 48 text

#jjug_ccc #jjug_ccc_a1 critical?

Slide 49

Slide 49 text

#jjug_ccc #jjug_ccc_a1 critical()がコード実行に与える影響

Slide 50

Slide 50 text

#jjug_ccc #jjug_ccc_a1 Linker#critical(boolean) Linker.Option (Java SE 22 & JDK 22) (oracle.com) Critical Functionとは: • どのような状況でも 実行時間がものすごく短い • upcallしない パフォーマンスゲインを 得やすいが… 一歩間違うと パフォーマンス劣化や JVMのクラッシュを 引き起こす可能性大

Slide 51

Slide 51 text

#jjug_ccc #jjug_ccc_a1 criticalがGCとバッティングすると… sleep(3)をcriticalで呼び出した裏で、定期的にGCを回す

Slide 52

Slide 52 text

#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)

Slide 53

Slide 53 text

#jjug_ccc #jjug_ccc_a1 わざとクラッシュさせて、コアからコールスタックを見てみます Linuxでabort()を呼びます critical()の作用する場所

Slide 54

Slide 54 text

#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 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と呼び出し関数の橋渡し部分)

Slide 55

Slide 55 text

#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~スタブコード~ スタブコードが全然違う critical non-critical

Slide 56

Slide 56 text

#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~スタブコード~ criticalで省略されるもの:Thread Stateの変更 • _thread_in_java ⇔ _thread_in_native • 制御レジスタなどの復元 • メモリバリアの設定 • Safepointのポーリング

Slide 57

Slide 57 text

#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~パフォーマンス~ • RDTSCを呼び出し結果を1つの64ビット値にまとめて返すだけの 実行パフォーマンスをJMHで測定する • FFMは同じMemorySegmentをcriticalとnon-criticalのMethodHandleを 作成して呼び出す ffmasm/benchmarks/funccall

Slide 58

Slide 58 text

#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~パフォーマンス~

Slide 59

Slide 59 text

#jjug_ccc #jjug_ccc_a1 ヒープメモリアクセス critical()利用の大きなメリット

Slide 60

Slide 60 text

#jjug_ccc #jjug_ccc_a1 ヒープメモリアクセス • critical()の引数にtrueを与えると、呼び出した関数から 直接Javaヒープ上のアドレスを参照できる • MemorySegmentを作ってコピーして処理して 最後にJavaヒープに戻す、ということをやらなくて済む • ただし、MethodHandleに渡すのはMemorySegmentになるため パラメータはMemorySegment.ofArray()で作る必要がある

Slide 61

Slide 61 text

#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にする

Slide 62

Slide 62 text

#jjug_ccc #jjug_ccc_a1 配列オブジェクトを破壊してみる Panamaを先取り!? JVMCIでJITと遊ぶ Javaの配列は中身の直前に (C++の)int で 長さが入っている

Slide 63

Slide 63 text

#jjug_ccc #jjug_ccc_a1 配列オブジェクトを破壊してみる Java配列アドレス (MemorySegment)-4 に位置する “長さ” をゼロに上書きする new int[100] したのに 関数コール後に array.length が ゼロになっている!

Slide 64

Slide 64 text

#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~SIMDパフォーマンス~ • 8要素の固定長int配列の足し算の実行パフォーマンスをJMHで測定 • 同じMemorySegmentをcriticalとnon-criticalのMethodHandleを 作成して呼び出す • critical()の引数はtrueに設定する ffmasm/benchmarks/vectorapi

Slide 65

Slide 65 text

#jjug_ccc #jjug_ccc_a1 critical VS non-critical ~SIMDパフォーマンス~ ※素のJava呼び出しやVector APIとの比較は、ぜひご自身で確認を!

Slide 66

Slide 66 text

#jjug_ccc #jjug_ccc_a1 FFMでいろいろできるのはわかったけど MethodHandleが分かりにくい JNIみたいにできれば 可読性も上がるのに…

Slide 67

Slide 67 text

#jjug_ccc #jjug_ccc_a1 FFM生成コードを JNI関数としてバインドする

Slide 68

Slide 68 text

#jjug_ccc #jjug_ccc_a1 JNI関数が動くまで ライブラリをロードする(System.loadLibrary()など) JNI関数を実行しようとする ライブラリからJNI関数を見つける(初回実行時のみ) 関数ポインタをnativeメソッドと紐づける(初回実行時のみ) 実行する 初回実行時にnativeメソッドと関数ポインタをバインドする …

Slide 69

Slide 69 text

#jjug_ccc #jjug_ccc_a1 もう1つの方法:JNI関数の動的バインド • JNI関数のRegisterNatives()を使えば 動的に関数ポインタをnativeメソッドと 紐づけることができる • クラスライブラリの実装でも よくやっている手法 • 登録には以下の情報が必要 • 登録するクラスのjclass • 登録するメソッドの名前 • 登録するメソッドのシグネチャ • 紐づける関数ポインタ Java Native Interface Specification: 4 - JNI Functions (oracle.com)

Slide 70

Slide 70 text

#jjug_ccc #jjug_ccc_a1 動的バインド ライブラリをロードするなりメモリ上に展開するなりする RegisterNatives()でnativeメソッドに関数ポインタを紐づける RegisterNatives()でnativeメソッドを関数ポインタとバインドする

Slide 71

Slide 71 text

#jjug_ccc #jjug_ccc_a1 jclass • JNIでClassを表現する型 • FindClass()で取得可能 Java Native Interface Specification: 4 - JNI Functions (oracle.com)

Slide 72

Slide 72 text

#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()を呼び出したネイティブメソッドに紐づいた クラスローダを検出する

Slide 73

Slide 73 text

#jjug_ccc #jjug_ccc_a1 FindClass()が使えない!? src/hotspot/share/prims/jni.cpp 自分のクラスローダに紐づいた クラスしか探索できないため 汎用性を持たせられない!

Slide 74

Slide 74 text

#jjug_ccc #jjug_ccc_a1 オレたちにはJVMTIがあるじゃないか!

Slide 75

Slide 75 text

#jjug_ccc #jjug_ccc_a1 Java Virtual Machine Tool Interface • Javaのデバッグや監視に便利な インターフェース(API) • ネイティブな共有ライブラリ形式 • java起動引数でアタッチする • jcmdで動的にアタッチする • 主な機能 • JVM内部イベントのフック • クラス書き換え • オブジェクトの参照関係追跡 JVM(TM) Tool Interface 22.0.0 (oracle.com)

Slide 76

Slide 76 text

#jjug_ccc #jjug_ccc_a1 なぜJVMTIなのか クラスローダ関係なく JVM中に存在するクラスを すべて取得してくれる! JVM(TM) Tool Interface 22.0.0 (oracle.com) GetLoadedClasses()があるから

Slide 77

Slide 77 text

#jjug_ccc #jjug_ccc_a1 JNIをバインドするまで GetLoadedClasses()で、ロード済み全jclassを取得する 目的のjclassを見つける RegisterNatives()でJNI関数を登録する FFM FFM Java

Slide 78

Slide 78 text

#jjug_ccc #jjug_ccc_a1 高い壁:JNI参照 GetLoadedClasses()で返されるjclassはJNIのローカル参照! 一度ネイティブ呼び出しから抜けるとjclassが無効になってしまう! src/hotspot/share/prims/jvmtiGetLoadedClasses.cpp src/hotspot/share/prims/jvmtiEnvBase.cpp

Slide 79

Slide 79 text

#jjug_ccc #jjug_ccc_a1 JNIをバインドするまで せっかくのjclassが無効になってしまう! 1度Javaの世界を経由する GetLoadedClasses()で、ロード済み全jclassを取得する 目的のjclassを見つける RegisterNatives()でJNI関数を登録する FFM FFM Java

Slide 80

Slide 80 text

#jjug_ccc #jjug_ccc_a1 JNIをバインドするまで GetLoadedClasses()で、ロード済み全jclassを取得する 目的のjclassを見つけ、RegisterNatives()呼び出し準備をする RegisterNatives()でJNI関数を登録する 全部を一息に終わらせる処理をFFMで作り Javaでやりたい部分はupcallで呼び出す! FFM upcall

Slide 81

Slide 81 text

#jjug_ccc #jjug_ccc_a1 JavaVMとJNIEnvとjvmtiEnv • JNI関数、JVMTI関数の実行には それぞれJNIEnv、jvmtiEnvが 必要 • C++の クラスインスタンスのようなもの • thisポインタのように 必ず 引数に与えなければならない • これらのインスタンスは JavaVMインスタンスから取得する JavaVM JNIEnv jvmtiEnv GetEnv(JNI version) GetEnv(JVMTI version)

Slide 82

Slide 82 text

#jjug_ccc #jjug_ccc_a1 JavaVMとJNIEnvとjvmtiEnv • これらのインスタンスに含まれる関数には すべてインデックス番号がついている • 実体はCの構造体のため そこの関数テーブルの インデックスを表している • これらを地道に解決して 呼び出していく…

Slide 83

Slide 83 text

#jjug_ccc #jjug_ccc_a1 あまりにも大変&地道に呼ぶだけなので… ffmasmの実装を見てください! ffmasm/src/main/java/com/yasuenag/ffmasm/internal • JavaVM.java • JniEnv.java • JvmtiEnv.java 注目ポイント: 関数テーブルを探す→呼び出す ※繰り返し

Slide 84

Slide 84 text

#jjug_ccc #jjug_ccc_a1 “一息” の全体像 ffmasm/src/main/java/com/yasuenag/ffmasm/internal/amd64/AMD64NativeRegister.java GetLoadedClass() upcall jclass探索と RegisterNatives() GetLoadedClass()で 確保したメモリの クリーンアップ

Slide 85

Slide 85 text

#jjug_ccc #jjug_ccc_a1 Shadow Space • デバッグ目的など、Windowsで レジスタ渡しの4つの引数を 格納するためのスタック領域 • 64bitレジスタ×4 = 32バイトが 必要 • 呼び出し直前にRSPが16バイトで アラインされている必要がある x64 でのスタックの使用 | Microsoft Learn これが守られていないと call命令発行時にJVMがクラッシュする!

Slide 86

Slide 86 text

#jjug_ccc #jjug_ccc_a1 upcallの内容 ffmasm/src/main/java/com/yasuenag/ffmasm/NativeRegister.java クラス探し (ループ) クラス名比較 (strcmp()) RegisterNatives()

Slide 87

Slide 87 text

#jjug_ccc #jjug_ccc_a1 JNI関数として登録する場合の注意点 • 第1引数は必ずJNIEnvへのポインタ • 第2引数は何らかのインスタンス • インスタンスメソッドの場合:thisインスタンス • staticメソッドの場合:classインスタンス JNI関数をバインドする場合は 実質第3引数からしか使えない!

Slide 88

Slide 88 text

#jjug_ccc #jjug_ccc_a1 パフォーマンス • 先ほどご紹介したRDTSCの例で計測 • JNI関数はそこに入る時点で_thread_in_nativeへの ステート遷移が発生するので、critical関数として登録 素のJNI呼び出しとFFMのJNIバインドは スコアがほぼ変わらない!

Slide 89

Slide 89 text

#jjug_ccc #jjug_ccc_a1 MethodHandleの定義次第では… criticalが遅い… ffmasm リビジョン fff651f でのベンチ結果

Slide 90

Slide 90 text

#jjug_ccc #jjug_ccc_a1 JMH実行時のJNIとFFMのコールパス JMH実行時に -prof async でFlameGraphを見てみると… JNI呼び出し FFM呼び出し ※ffmasm リビジョン fff651f でのベンチ結果

Slide 91

Slide 91 text

#jjug_ccc #jjug_ccc_a1 static finalか否かでのインライン化の違い 修正後:MethodHandleがstatic final 修正前:MethodHandleが普通のクラスメンバ インライン化される対象が 全然違う!

Slide 92

Slide 92 text

#jjug_ccc #jjug_ccc_a1 static finalのときのみに存在するログ ネイティブ呼び出し部分まで インライン化される!

Slide 93

Slide 93 text

#jjug_ccc #jjug_ccc_a1 HotSpotから見たstatic finalフィールドの扱い 定数フィールドとして 扱われるため より最適化が効きやすくなる! src/hotspot/share/ci/ciField.cpp

Slide 94

Slide 94 text

#jjug_ccc #jjug_ccc_a1 まとめ

Slide 95

Slide 95 text

#jjug_ccc #jjug_ccc_a1 まとめ • FFMを使えばJavaだけで動的に機械語を生成して実行可能 • ただし、いくつか考慮は必要 • 実行用メモリ空間 • 呼び出し規約 • 命令エンコーディング • critical()使用時のJVMに与える影響 • JNI/JVMTI関数呼び出し • critical()で関数の呼び出しコストを下げられる • ただし、MethodHandleの定義の仕方やJITのかかり方に注意

Slide 96

Slide 96 text

#jjug_ccc #jjug_ccc_a1 まとめ ※大事なこと 気合と根性で何とかなる!

Slide 97

Slide 97 text

#jjug_ccc #jjug_ccc_a1 最後に • ffmasmの協力者募集中です! • さらなる命令拡充 • Arm64対応 • バグフィックス • ドキュメント • …

Slide 98

Slide 98 text

#jjug_ccc #jjug_ccc_a1