概要
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非準拠だが実用上安定、パフォーマンスが重要な場面では非常に有用な知見。