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

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

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

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

JJUG CCC 2024 Spring講演資料

Avatar for Yasumasa Suenaga

Yasumasa Suenaga

June 16, 2024
Tweet

More Decks by Yasumasa Suenaga

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のかかり方に注意