世界を動かす技術を、日本語で。

40行の修正で400倍のパフォーマンスギャップを解消

2026年1月14日原文(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非準拠だが実用上安定、パフォーマンスが重要な場面では非常に有用な知見。

Hackerたちの意見

著者です。前回のカーネルバグについての投稿の後、JVMがスレッドのアクティビティをどう報告しているかを調べてみました。「このスレッドのCPU時間はどれくらいですか?」という質問は、思ったよりもずっと高コストな質問だったことがわかりました。

分布の大きなばらつきについて調べてみましたか?中には何桁も跨っているものがあって、面白いですね。

ナノ秒の分数について話すには、時計の安定性と精度を非常に良く理解している必要があると思います。せいぜい何らかの減少があると主張できるかもしれませんが、測定された時間が本当に正確であることを確認するために大量の準備作業をしない限り、絶対的な主張はすごく難しいです。大きな誤差があるかもしれないし、その違いに気づかないこともあります。だから、これらの測定に隠れた原子時計が関与していない限り、何らかの形で条件を付けるべきだと思います。

1行要約の編集に感謝!これについて考え直したんだけど、低品質なコメントになっちゃった;そんなTLDRを出すことで、特にHNではコンテンツを読む価値が大いに増すよね。短い形式で読むのは、まるで先に教えてくれるクールな友達みたいな感じ。

なんてサプライズだ。

すごくいいまとめですね!

clock_gettime()はvDSOを通って、コンテキストスイッチを回避します。フレームグラフにも表示されますよ。

編集:ああ、違う。CLOCK_THREAD_CPUTIME_IDはvDSOを通ってカーネルに行くので、タスク構造体を見なければならないのは理解できます。ここでタスク構造体を取得します:https://elixir.bootlin.com/linux/v6.18.5/source/kernel/time/... そしてここ:https://elixir.bootlin.com/linux/v6.18.5/source/kernel/time/... ここで実際に値を取り出します:https://elixir.bootlin.com/linux/v6.18.5/source/kernel/sched... ここがvDSOの時計選択ロジック:https://elixir.bootlin.com/linux/v6.18.5/source/lib/vdso/get... もしvDSO時計でない場合は、ここがシステムコールへのフォールバックです:https://elixir.bootlin.com/linux/v6.18.5/source/lib/vdso/get...

一部の時計(CLOCK_MONOTONICなど)と一部の時計ソースに限ります。VIRT/SCHEDの場合、vDSOシムは実際のシステムコールを呼び出さなければなりません。スレッドごとのアカウンティングが必要な場合、カーネルの遷移を避けることはできません。

vDSOフレームの下を見ると、まだsyscallがあるね。この特定のクロックIDに対するvDSOの実装には、早いパスが欠けてると思う(実装できるかもしれないけど)。

ソフトウェアのperfイベントを使うと、もっと速く、約8ns(ほぼ10倍の改善)になるよ。PERF_COUNT_SW_TASK_CLOCKはスレッドのCPU時間で、共有ページを通じて読み取れるからsyscallは不要(perf_event_mmap_pageを参照)。それから、最後のコンテキストスイッチ以降のデルタを、seqlock内の単一のrdtsc呼び出しで加算するんだ。残念ながら、これはあまり文書化されてないし、オープンソースの実装も知らない。追記:でも、PERF_COUNT_SW_TASK_CLOCKがユーザー時間だけを選択できるかは分からない。カーネルは確実にできるけど、配線があるかは不明。ただ、これが全体のスレッドCPU時間には確実に効果があるよ。

それは素晴らしいトリックだね。perf_eventのセットアップオーバーヘッドや権限の要件は、任意のスレッドには重いかもしれないけど、長寿命のスレッドにはかなり素晴らしいと思う!シェアしてくれてありがとう!

Hacker Newsで議論の続きを見る