30 ポイント 投稿者 GN⁺ 2026-01-15 | 5件のコメント | WhatsAppで共有
  • 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 による追加最適化

  • カーネルは clockidPID=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件のコメント

 
crawler 2026-01-15

すごいですね。

> 2倍速くなったなら賢いやり方をしたのかもしれないし、100倍速くなったなら愚かなことをやめただけ

まったく的外れな言葉ではないと思いますが、カーネルと絡むケースでは、遅いことに気づくこと自体が本当に難しかっただろうと思います。

 
[このコメントは非表示になっています。]
 
princox 2026-01-19

こういうものは、プロジェクトの中でどうやって見つけられるのでしょうか? AIを回したからといって分かるものでもなさそうですが..

こういう事例を見ると、私も学んでぜひ経験してみたいと思います。

 
aobamisaki 2026-01-15

実際、コード全体を全面的に書き換えても2〜3倍の向上すら難しいのに、単に数行変えただけで最大400倍向上するのは本当にすごいですね。

 
GN⁺ 2026-01-15
Hacker News のコメント
  • 投稿者本人です。前回のカーネルバグに関する記事のあと、JVM がスレッド活動をどのように自前で報告しているのかを調べてみた
    「このスレッドの CPU 使用時間はいくらか?」という問いが、思った以上に 非常に高コストな処理 だと分かった
    • ナノ秒単位の測定を論じるなら、クロックの安定性と正確性 を非常によく理解している必要がある
      原子時計レベルの基準がなければ、絶対的な数値を主張するのは難しいと思う
    • 分布が複数の 桁にまたがって広がっている理由 を調べたのか気になる。それ自体が興味深い現象だ
    • 短い TL;DR 要約 が本当にありがたかった。こうした要約は記事への心理的ハードルを下げ、読む動機を作ってくれる
    • 「驚くことでもない(Quelle Surprise)」という反応を残した
  • clock_gettime()vDSO によってコンテキストスイッチを回避する。だから flamegraph にもその痕跡が見える
    • ただし一部の clock に限られる。CLOCK_VIRTCLOCK_SCHED のようなものは依然として syscall 呼び出し が必要だ
    • vDSO フレームの下を見ると、まだ syscall がある。特定の clock id 向けの 高速経路(fast path) が実装されていないようだ
    • 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 は本当に素晴らしいツールだ
    コードだけ見れば問題なさそうでも、flamegraph を見ると「これは何だ?!」となることがよくある
    静的初期化ではない初期化や、1 行のロガー呼び出しが 高コストなシリアライズ を引き起こすなど、さまざまな問題を見つけた
    • 私は icicle graph も好きだ。flamegraph とは逆方向に積み上がるので、複数の経路が共通ライブラリを呼ぶときのボトルネックを見やすい
    • この SVG の例 を新しいタブで開くと インタラクティブなズーム ができる
    • パフォーマンスプロファイリングと最適化の実験は、開発で最も楽しい部分のひとつだ。「なぜこれがこんなに遅いんだ?」という驚きが多い
    • 文字列パースと memoization の組み合わせが奇妙に聞こえるという意見もあった。実際には高コストな正規表現パターンのパースをキャッシュしていなかったのが問題だった
    • flamegraph を初めて使ってみようという人に、基本概念と始め方を尋ねている
  • 「画像を新しいタブで開く」が実際に SVG のインタラクション を提供することに驚いた
    • この機能は Brendan Gregg の FlameGraph スクリプト のおかげ
      普段は async-profiler の HTML ジェネレータを使っているが、今回は単一の SVG のために Brendan のツールを使った
  • OpenJDK パッチの作者本人です。/proc を読む際の メモリオーバーヘッド と eBPF プロファイリング、そして十分に文書化されていない user-space ABI の歴史を扱った
    詳細は 私のブログ記事 にまとめた
    • なぜ元の実装がそうなっていたのか気になるという質問を受けた。呼び出しのたびにファイル I/O と文字列パースを行うのは非効率だが、当時は理由があったのだろうと思う
    • Jaromir が私の記事を見て「自分も同じ時期に草稿を書いた」と言い、お互いの記事をリンク した。私の記事のほうがより厳密だと評価してくれてうれしかった
  • C や C++ のようなシステム言語だからといって常に速いわけではない。何をするか によって速度は大きく変わる
  • vDSO 経由の読み取りは、カーネル遷移、バッファのシリアライズ、パース処理を避けるため はるかに高速
  • 「2 倍速くなったなら賢いことをしたのかもしれないし、100 倍速くなったなら 愚かなことをやめただけ だ」という引用を共有
    元ツイート
  • QuestDB チームはこの分野で 最高レベル だ。人もソフトウェアもどちらも素晴らしい
    Jaromir のブログも本当に良かった