40行の修正で400倍のパフォーマンスギャップを解消
93日前原文(questdb.com)
概要
OpenJDKのThreadMXBean.getCurrentThreadUserTime()のLinux実装が大幅に高速化。 /procファイル読み取りからclock_gettime()への移行による40倍のパフォーマンス向上。 Linux固有のclockid_tビット操作でユーザーCPU時間のみ取得可能に。 カーネルの高速パス利用で更なる最適化の可能性。 このビット操作は公式ドキュメントに乏しいが、20年以上安定運用。
OpenJDKのスレッドCPU時間取得の劇的高速化
- OpenJDKのThreadMXBean.getCurrentThreadUserTime()は、Linux上で/proc/self/task/<tid>/statファイルをパースしてユーザーCPU時間を取得していた従来実装。
- この方法は複数のシステムコール、VFS経由のファイルI/O、カーネル側での文字列生成、ユーザー空間での複雑なパース処理を必要とし、非常に低速。
- 比較対象のgetCurrentThreadCpuTime()は**clock_gettime(CLOCK_THREAD_CPUTIME_ID)**を直接呼び出すだけで、ファイルI/Oもパースも不要。
- ベンチマークでは従来実装が平均11マイクロ秒、新実装では279ナノ秒に短縮、40倍の高速化を達成。
- 並列実行時はカーネルリソースのロック競合も発生しやすく、/proc方式はさらに不利。
なぜ2つの実装があったのか
- POSIX標準ではCLOCK_THREAD_CPUTIME_IDはユーザー+システム時間の合計のみ取得可。
- ユーザー時間のみ取得するポータブルな方法は標準化されていないため、/proc方式が使われていた経緯。
- Linux固有の内部仕様を活用することで、より効率的な実装が可能に。
Linuxカーネルのclockid_tビット操作
- Linux 2.6.12以降、clockid_t値にクロック種別情報をビットでエンコード。
- Bit 2: スレッド vs プロセスクロック
- Bits 1-0: クロック種別(00=PROF, 01=VIRT(ユーザー時間のみ), 10=SCHED(合計), 11=FD)
- 残りビットはPID/TIDをエンコード
- pthread_getcpuclockid()はPOSIX準拠のSCHED(合計)クロックIDを返すが、下位ビットを01(VIRT)に変更することでユーザー時間のみ取得可能。
- 新実装ではこのビット操作を用い、ファイルI/Oやパースを完全排除。
カーネルの高速パス活用による更なる最適化
- clockid_tにPID=0(現在のスレッド)をエンコードすると、カーネルはradix tree探索をスキップし、直接カレントタスクにアクセス。
- pthread_getcpuclockid()経由ではTIDがセットされるため、毎回radix tree探索が発生。
- clockid_tを手動で構築しPID=0を指定すれば、より高速なパスが利用可能。
- JVMは既にclockidのビット操作を行っているため、完全自作も技術的には問題なし。
公式ドキュメントと安定性
- このclockid_tのビット仕様はmanページ等の公式ドキュメントにはほぼ記載なし。
- カーネルソースコードやCPU_CLOCK_*マクロでのみ確認可能。
- 20年以上仕様変更なし、glibcも依存しているため今後も安定運用が見込まれる。
まとめ
- OpenJDKのLinux実装は、Linuxカーネルの内部仕様を活用してスレッドCPU時間取得を大幅高速化。
- さらにカーネルの高速パスを活かす余地も存在。
- POSIX非準拠だが実用上安定、パフォーマンスが重要な場面では非常に有用な知見。