[インデックス 19289] ファイルの概要
このコミットは、GoランタイムのCPUプロファイラに関連するバグ修正です。具体的には、CPUプロファイリング中に失われたサンプル数のカウントが不正確であった問題と、未使用の変数の削除が行われました。変更が加えられたファイルは src/pkg/runtime/cpuprof.goc
であり、これはGoランタイムにおけるCPUプロファイリング機能の核心部分を担っています。
コミット
runtime: cpuプロファイラのバグを修正 失われたサンプル数が過剰にカウントされていた(リセットされていなかった)。 また、未使用の変数を削除(デバッグのために必要であれば簡単に復元できる)。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c0bf96e6b10976274bf5ee7813845dc0eb590816
元コミット内容
commit c0bf96e6b10976274bf5ee7813845dc0eb590816
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed May 7 18:48:14 2014 +0400
runtime: fix bug in cpu profiler
Number of lost samples was overcounted (never reset).
Also remove unused variable (it's trivial to restore it for debugging if needed).
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews, rsc
https://golang.org/cl/96060043
変更の背景
GoのCPUプロファイラは、プログラムの実行中にCPUがどの関数で時間を費やしているかを特定するために使用されます。これは、パフォーマンスのボトルネックを特定し、最適化を行う上で非常に重要なツールです。プロファイラは通常、一定の間隔で実行中のスタックトレースをサンプリングすることで機能します。しかし、システムが非常にビジーである場合や、プロファイラ自体のオーバーヘッドを最小限に抑えるために、すべてのサンプリングイベントを記録できないことがあります。このような場合、記録できなかったサンプルは「失われたサンプル(lost samples)」としてカウントされます。
このコミット以前のGoランタイムのCPUプロファイラには、失われたサンプル数を正確に報告できないバグが存在しました。具体的には、Profile
構造体内のlost
フィールド(失われたティック数を記録するカウンタ)が、プロファイリングデータがログにフラッシュされた後もリセットされていませんでした。このため、複数のプロファイリング期間にわたってlost
カウンタが累積され続け、結果として失われたサンプル数が過剰に、かつ不正確に報告されるという問題が発生していました。
また、totallost
という変数も存在しましたが、これはlost
が適切にリセットされれば不要となるか、あるいはデバッグ目的で一時的に使用されていたものの、本番コードには必要ない未使用の変数となっていました。このコミットは、これらの問題を解決し、CPUプロファイリングデータの正確性を向上させることを目的としています。
前提知識の解説
CPUプロファイリング
CPUプロファイリングとは、プログラムがCPU時間をどのように消費しているかを分析する手法です。これにより、アプリケーションのどの部分が最も計算コストが高いか(ホットスポット)を特定し、パフォーマンス最適化の対象を絞り込むことができます。一般的なCPUプロファイラは、以下のいずれかの方法で動作します。
- サンプリングプロファイリング: 一定の時間間隔(例: 1ミリ秒ごと)でプログラムの実行を一時停止し、現在の実行スタック(コールスタック)を記録します。これにより、CPUがどの関数で時間を費やしているかの統計的な分布を得ることができます。GoのCPUプロファイラはこの方式を採用しています。
- インストゥルメンテーションプロファイリング: コードに明示的な計測ポイント(インストゥルメンテーション)を挿入し、関数の開始・終了時間やループの反復回数などを記録します。
サンプリングプロファイリングは、オーバーヘッドが低く、コードの変更なしに適用できるため、本番環境での使用に適しています。
Goランタイム
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクタ、スケジューラ(ゴルーチンの管理)、メモリ管理、ネットワークI/O、そしてプロファイリングツールなどが含まれます。Goプログラムは、オペレーティングシステム上で直接実行されるのではなく、Goランタイムによって抽象化された環境で動作します。src/pkg/runtime/
ディレクトリには、このランタイムのコア部分がC(*.goc
ファイル)とGo(*.go
ファイル)で実装されています。
Goのプロファイラ (pprof
)
Goには、標準で強力なプロファイリングツールセットが組み込まれており、pprof
というコマンドラインツールを通じて利用できます。pprof
は、CPUプロファイル、メモリプロファイル、ブロックプロファイル、ミューテックスプロファイルなど、様々な種類のプロファイルを視覚化・分析する機能を提供します。
CPUプロファイルは、runtime/pprof
パッケージを使用して生成されます。プログラム内でpprof.StartCPUProfile()
とpprof.StopCPUProfile()
を呼び出すことで、指定された期間のCPUプロファイルデータを収集できます。このデータは、内部的にはGoランタイムが定期的にサンプリングしたスタックトレースの集合であり、cpuprof.goc
のようなファイルでそのサンプリングロジックが実装されています。
サンプルロス (Lost Samples)
プロファイリングにおいて「サンプルロス」とは、プロファイラが何らかの理由でサンプリングイベントを記録できなかった場合に発生する現象です。これは、プロファイラ自体のオーバーヘッドを抑えるため、あるいはシステムが非常に高負荷で、サンプリング処理が間に合わない場合に起こり得ます。失われたサンプルは、プロファイリングデータの完全性を損なう可能性がありますが、プロファイラの設計によっては、その数を追跡し、報告することで、プロファイリングデータの信頼性に関する情報を提供することがあります。このコミットのバグは、まさにこの「失われたサンプル」のカウントが不正確であったことに起因します。
技術的詳細
このコミットで変更された src/pkg/runtime/cpuprof.goc
ファイルは、GoランタイムのCPUプロファイリング機能の中核をなすC言語で書かれた部分です。
Profile
構造体
CPUプロファイリングの状態を管理する Profile
構造体には、以下のようなフィールドが含まれていました。
struct Profile {
uintptr count; // tick count
uintptr evicts; // eviction count
uintptr lost; // lost ticks that need to be logged
uintptr totallost; // total lost ticks
// ...
};
lost
: 現在のプロファイリング期間中に失われたサンプル(ティック)の数を記録します。この値は、プロファイリングデータがログにフラッシュされる際に使用されます。totallost
: このコミットで削除された変数です。以前は、lost
の累積合計を保持しようとしていた可能性がありますが、lost
が適切にリセットされない限り、その目的を果たすことはできませんでした。
add
関数
add
関数は、新しいCPUプロファイルサンプル(スタックトレース)をプロファイラに追加する役割を担っています。この関数内で、プロファイラが新しいエントリを記録できなかった場合(例: ハッシュテーブルがいっぱいの場合など)、サンプルが失われたと判断され、p->lost
とp->totallost
がインクリメントされていました。
// add(Profile *p, uintptr *pc, int32 n) の一部
if(!evict(p, e)) {
// Could not evict entry. Record lost stack.
p->lost++;
p->totallost++; // この行が削除された
return;
}
flushlog
関数
flushlog
関数は、収集されたプロファイリングデータをログにフラッシュする役割を担っています。この関数が呼び出されると、p->lost
の値がログに記録されます。しかし、このコミット以前は、p->lost
がログに記録された後も0にリセットされていませんでした。
// flushlog(Profile *p) の一部 (変更前)
if(p->lost > 0) {
*q++ = p->lost;
*q++ = 1;
*q++ = (uintptr)LostProfileData;
// p->lost のリセットがここになかった
}
このp->lost
のリセット漏れが、失われたサンプル数の過剰カウントの根本原因でした。プロファイリングが継続されると、p->lost
は前回の期間の失われたサンプル数を保持したまま、さらに新しい失われたサンプル数を加算し続けていました。
コアとなるコードの変更箇所
--- a/src/pkg/runtime/cpuprof.goc
+++ b/src/pkg/runtime/cpuprof.goc
@@ -81,7 +81,6 @@ struct Profile {
uintptr count; // tick count
uintptr evicts; // eviction count
uintptr lost; // lost ticks that need to be logged
- uintptr totallost; // total lost ticks
// Active recent stack traces.
Bucket hash[HashSize];
@@ -244,7 +243,6 @@ add(Profile *p, uintptr *pc, int32 n)
if(!evict(p, e)) {
// Could not evict entry. Record lost stack.
p->lost++;
- p->totallost++;
return;
}
p->evicts++;
@@ -308,6 +306,7 @@ flushlog(Profile *p)
*q++ = p->lost;
*q++ = 1;
*q++ = (uintptr)LostProfileData;
+ p->lost = 0;
}
p->nlog = q - log;
return true;
コアとなるコードの解説
このコミットによる変更は、主に以下の2点です。
-
totallost
フィールドの削除:Profile
構造体からuintptr totallost;
の行が削除されました。これに伴い、add
関数内でp->totallost++;
の行も削除されています。コミットメッセージにあるように、この変数は未使用であったか、あるいはlost
が適切に機能すれば不要となる変数でした。デバッグ目的で一時的に導入された可能性もありますが、本番コードには必要ないと判断されました。これにより、コードの簡潔性が向上し、不要な状態管理がなくなりました。 -
p->lost = 0;
の追加:flushlog
関数内で、失われたサンプル数p->lost
がログに記録された直後にp->lost = 0;
が追加されました。これはこのコミットの最も重要な変更点です。 この変更により、プロファイリングデータがログにフラッシュされるたびに、lost
カウンタがリセットされるようになりました。これにより、各プロファイリング期間における失われたサンプル数が正確にカウントされ、以前のように累積されて過剰に報告されることがなくなります。結果として、CPUプロファイリングデータの信頼性と正確性が大幅に向上しました。
これらの変更は、GoのCPUプロファイラがより正確なパフォーマンスデータを提供できるようになり、開発者がアプリケーションのボトルネックを特定する際に、より信頼性の高い情報に基づいた意思決定を行えるようになることを意味します。
関連リンク
- Go プロファイリングの公式ドキュメント: https://go.dev/doc/diagnose-cpu-mem
runtime/pprof
パッケージ: https://pkg.go.dev/runtime/pprof
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goのソースコード (特に
src/runtime/
ディレクトリ) - 一般的なCPUプロファイリングの概念に関する知識
- コミットメッセージと差分情報