Upgrade to Pro — share decks privately, control downloads, hide ads and more …

FFMでJITコンパイラを作ってみた

 FFMでJITコンパイラを作ってみた

JJUG CCC 2024 Spring講演資料

Yasumasa Suenaga

June 16, 2024
Tweet

Other Decks in Technology

Transcript

  1. #jjug_ccc #jjug_ccc_a1 似た話 Panamaを先取り!? JVMCIでJITと遊ぶ • JVM Compiler Interface •

    JavaでJITを作るための インターフェース • Graal Compilerでも利用 • Java Day Tokyo 2017で 講演しました
  2. #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年かけて完成!
  3. #jjug_ccc #jjug_ccc_a1 FFMによる外部関数の呼び出し メモリ ① ライブラリ ② ③ ① メモリにライブラリをロードする→SymbolLookupを得る

    ② 目的の関数がある場所を見つける→MemorySegmentを得る ③ 見つけた場所を呼び出す→MethodHandleをinvokeする
  4. #jjug_ccc #jjug_ccc_a1 Just In Timeコンパイラ • 実行時にプログラムを機械語へコンパイルする機能 • JavaのHotSpotや.NETのRyuJIT、JSのV8など 様々な言語ランタイムで導入されている

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

    • HotSpotの場合 • “Hot”と判断されたメソッドが実行状況を加味して 最適化されたうえで機械語へコンパイルされる • 最適化のレベルが細分化されている(Tiered Compilation) • 配列コピーやMD5ハッシュ値計算などの「処理」のほか 例外キャッチや引数処理など内部処理効率化のコード生成も行う 本日の中心
  6. #jjug_ccc #jjug_ccc_a1 “JIT生成コードが動く” を分解してみる メモリ ① 実行可能メモリ ① 実行可能メモリをアロケートする ②

    機械語を生成し、実行可能メモリに書き込む ③ 書き込んだ機械語の先頭アドレスを呼び出す ② ③ ライブラリの ロードと等価
  7. #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現在のものです
  8. #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
  9. #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
  10. #jjug_ccc #jjug_ccc_a1 意識しなければならないこと • 呼び出し規約 • 関数の呼び出しと戻るときの “お約束” • 処理系に強く依存

    • 命令のエンコーディング • アセンブラコードを、どのように0と1で表すか? • CPUによって異なる • OSは関係なし
  11. #jjug_ccc #jjug_ccc_a1 命令のエンコーディング:Intel 64の場合 • Intel® 64 and IA-32 Architectures

    Software Developer's Manual • 略して “SDM” • 自分が動かしたいIntel 64のアセンブラコードを どう機械語に落とし込めばいいかのすべてが書かれている • 単純な命令であってもプリフィックスとModR/Mは要注意
  12. #jjug_ccc #jjug_ccc_a1 書き込み 直接MemorySegmentに書き込んでもよいですが… ByteBuffer経由の書き込みがオススメ! • 理由:ラクだから • 様々なデータ型をput()で書き込める •

    開始アドレスさえ決まればインデックスを意識しなくていい • 注意点 • マルチバイトなデータはネイティブバイトオーダーで書き込む必要がある
  13. #jjug_ccc #jjug_ccc_a1 Linker#critical(boolean) Linker.Option (Java SE 22 & JDK 22)

    (oracle.com) Critical Functionとは: • どのような状況でも 実行時間がものすごく短い • upcallしない パフォーマンスゲインを 得やすいが… 一歩間違うと パフォーマンス劣化や JVMのクラッシュを 引き起こす可能性大
  14. #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)
  15. #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と呼び出し関数の橋渡し部分)
  16. #jjug_ccc #jjug_ccc_a1 critical VS non-critical ~スタブコード~ criticalで省略されるもの:Thread Stateの変更 • _thread_in_java

    ⇔ _thread_in_native • 制御レジスタなどの復元 • メモリバリアの設定 • Safepointのポーリング
  17. #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にする
  18. #jjug_ccc #jjug_ccc_a1 もう1つの方法:JNI関数の動的バインド • JNI関数のRegisterNatives()を使えば 動的に関数ポインタをnativeメソッドと 紐づけることができる • クラスライブラリの実装でも よくやっている手法

    • 登録には以下の情報が必要 • 登録するクラスのjclass • 登録するメソッドの名前 • 登録するメソッドのシグネチャ • 紐づける関数ポインタ Java Native Interface Specification: 4 - JNI Functions (oracle.com)
  19. #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()を呼び出したネイティブメソッドに紐づいた クラスローダを検出する
  20. #jjug_ccc #jjug_ccc_a1 Java Virtual Machine Tool Interface • Javaのデバッグや監視に便利な インターフェース(API)

    • ネイティブな共有ライブラリ形式 • java起動引数でアタッチする • jcmdで動的にアタッチする • 主な機能 • JVM内部イベントのフック • クラス書き換え • オブジェクトの参照関係追跡 JVM(TM) Tool Interface 22.0.0 (oracle.com)
  21. #jjug_ccc #jjug_ccc_a1 JavaVMとJNIEnvとjvmtiEnv • JNI関数、JVMTI関数の実行には それぞれJNIEnv、jvmtiEnvが 必要 • C++の クラスインスタンスのようなもの

    • thisポインタのように 必ず 引数に与えなければならない • これらのインスタンスは JavaVMインスタンスから取得する JavaVM JNIEnv jvmtiEnv GetEnv(JNI version) GetEnv(JVMTI version)
  22. #jjug_ccc #jjug_ccc_a1 Shadow Space • デバッグ目的など、Windowsで レジスタ渡しの4つの引数を 格納するためのスタック領域 • 64bitレジスタ×4

    = 32バイトが 必要 • 呼び出し直前にRSPが16バイトで アラインされている必要がある x64 でのスタックの使用 | Microsoft Learn これが守られていないと call命令発行時にJVMがクラッシュする!
  23. #jjug_ccc #jjug_ccc_a1 まとめ • FFMを使えばJavaだけで動的に機械語を生成して実行可能 • ただし、いくつか考慮は必要 • 実行用メモリ空間 •

    呼び出し規約 • 命令エンコーディング • critical()使用時のJVMに与える影響 • JNI/JVMTI関数呼び出し • critical()で関数の呼び出しコストを下げられる • ただし、MethodHandleの定義の仕方やJITのかかり方に注意