- OpenJDK の
ThreadMXBean.getCurrentThreadUserTime() が、/proc ファイルのパースではなく clock_gettime() 呼び出しに置き換えられ、最大 400倍の性能向上を達成
- 従来の実装は
/proc/self/task/<tid>/stat ファイルを開いて読み取り、パースする複雑な I/O 経路を通っていた
- 新しい実装は Linux カーネルの
clockid_t ビットエンコーディングを活用し、pthread_getcpuclockid() で得た ID の下位ビットを調整して、ユーザー時間だけを直接参照
- ベンチマーク結果では平均呼び出し時間が 11μs → 279ns に減少し、その後カーネルの fast-path を適用すると 約13% の追加改善
- POSIX の制約を越えて、Linux 内部 ABI への理解を通じた最適化が可能であることを示す事例
既存実装の問題
getCurrentThreadUserTime() は /proc/self/task/<tid>/stat ファイルを開き、13番目と14番目のフィールドをパースして CPU ユーザー時間を計算
- ファイルパスの生成、ファイルオープン、バッファ読み取り、文字列パース、
sscanf() 呼び出しなど、多段階の処理が必要
- コマンド名に括弧が含まれる可能性があるため、
strrchr() で最後の ) を探す複雑なロジックも含む
- 一方
getCurrentThreadCpuTime() は、単一の clock_gettime(CLOCK_THREAD_CPUTIME_ID) 呼び出しだけを実行
- 2018年のバグレポート(JDK-8210452)によれば、両メソッドの速度差は 30〜400倍 に達していた
/proc アクセス経路と clock_gettime() 経路の比較
/proc 方式は open(), read(), sscanf(), close() など、複数のシステムコールとカーネル内部での文字列生成を含む
clock_gettime() 方式は 単一のシステムコールで sched_entity 構造体から直接時間値を読み取る
- 並列負荷時には
/proc へのアクセスで カーネルロック競合が発生し、遅延がさらに悪化する
新しい実装方式
- POSIX 標準では
CLOCK_THREAD_CPUTIME_ID は ユーザー時間+システム時間を返すよう定義されている
- Linux カーネルは
clockid_t の下位ビットで クロック種別をエンコードしている
00=PROF, 01=VIRT(ユーザー専用), 10=SCHED(ユーザー+システム)
pthread_getcpuclockid() で得た clockid の下位ビットを 01 に変更すると、ユーザー時間専用クロックへ切り替え可能
- 新しいコードではファイル I/O とパースを除去し、
clock_gettime() 呼び出しだけでユーザー時間を返す
性能測定結果
- 修正前の平均呼び出し時間は 11.186μs、修正後は 0.279μs で、約 40倍改善
- 16スレッド環境で測定され、もともと報告されていた 30〜400倍の範囲と一致
- CPU プロファイルでは ファイルのオープン・クローズ関連システムコールが消え、単一の
clock_gettime() 呼び出しだけが残る
カーネル fast-path による追加最適化
- カーネルは
clockid に PID=0 がエンコードされている場合、現在のスレッドへ直接アクセスする fast-path を提供
- JVM が
pthread_getcpuclockid() の代わりに clockid を直接構成して PID=0 を入れると、radix tree 探索を省略できる
- 手動構成した
clockid を使うと平均時間は 81.7ns → 70.8ns となり、約 13% の追加改善
- ただし
clockid_t のサイズなど、カーネル内部実装に依存するため、可読性と互換性の低下が懸念される
結論と教訓
- 40行の削除で 400倍の性能差を解消し、新しいカーネル機能なしに 既存 ABI の細部活用だけで達成
- カーネルソースを読み込む価値を強調: POSIX は移植性を保証する一方で、カーネルコードは 可能性の限界を示してくれる
- 既存の前提を見直す重要性:
/proc のパースは過去には合理的だったが、現在では非効率になっている
- この変更は JDK 26(2026年3月リリース予定)に含まれ、
ThreadMXBean.getCurrentThreadUserTime() 呼び出し時に 自動的な性能向上を提供
5件のコメント
すごいですね。
> 2倍速くなったなら賢いやり方をしたのかもしれないし、100倍速くなったなら愚かなことをやめただけ
まったく的外れな言葉ではないと思いますが、カーネルと絡むケースでは、遅いことに気づくこと自体が本当に難しかっただろうと思います。
こういうものは、プロジェクトの中でどうやって見つけられるのでしょうか? AIを回したからといって分かるものでもなさそうですが..
こういう事例を見ると、私も学んでぜひ経験してみたいと思います。
実際、コード全体を全面的に書き換えても2〜3倍の向上すら難しいのに、単に数行変えただけで最大400倍向上するのは本当にすごいですね。
Hacker News のコメント
「このスレッドの CPU 使用時間はいくらか?」という問いが、思った以上に 非常に高コストな処理 だと分かった
原子時計レベルの基準がなければ、絶対的な数値を主張するのは難しいと思う
clock_gettime()は vDSO によってコンテキストスイッチを回避する。だから flamegraph にもその痕跡が見えるCLOCK_VIRTやCLOCK_SCHEDのようなものは依然として syscall 呼び出し が必要だCLOCK_THREAD_CPUTIME_IDは結局カーネルに渡る。task struct を参照する必要があるためだ関連するカーネルソースは posix-cpu-timers.c、
cputime.c、
gettimeofday.c を参照
PERF_COUNT_SW_TASK_CLOCKを使うと約 8ns レベルの測定 も可能perf_event_mmap_pageを通じて共有ページから読み取り、rdtsc呼び出しでデルタを計算する方式あまり文書化されておらず、オープンソース実装もほとんどない
perf_eventの設定や権限要件が大きいため、長寿命のスレッド に向いていそうだseqlockが必要な理由を質問している。ページ値とrdtscの間で コンテキストスイッチ が起きないようにするためなのか気になるおそらく
rdtscの後にページ値を再確認し、変わっていたら再試行する構造に見えるちなみに
clock_gettimeも vdso ベースの仮想 syscall だclock_gettimeは syscall ではなく vdso を使うコードだけ見れば問題なさそうでも、flamegraph を見ると「これは何だ?!」となることがよくある
静的初期化ではない初期化や、1 行のロガー呼び出しが 高コストなシリアライズ を引き起こすなど、さまざまな問題を見つけた
普段は async-profiler の HTML ジェネレータを使っているが、今回は単一の SVG のために Brendan のツールを使った
/procを読む際の メモリオーバーヘッド と eBPF プロファイリング、そして十分に文書化されていない user-space ABI の歴史を扱った詳細は 私のブログ記事 にまとめた
元ツイート
Jaromir のブログも本当に良かった