Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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->lostp->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点です。

  1. totallost フィールドの削除: Profile 構造体から uintptr totallost; の行が削除されました。これに伴い、add 関数内で p->totallost++; の行も削除されています。コミットメッセージにあるように、この変数は未使用であったか、あるいはlostが適切に機能すれば不要となる変数でした。デバッグ目的で一時的に導入された可能性もありますが、本番コードには必要ないと判断されました。これにより、コードの簡潔性が向上し、不要な状態管理がなくなりました。

  2. p->lost = 0; の追加: flushlog 関数内で、失われたサンプル数 p->lost がログに記録された直後に p->lost = 0; が追加されました。これはこのコミットの最も重要な変更点です。 この変更により、プロファイリングデータがログにフラッシュされるたびに、lostカウンタがリセットされるようになりました。これにより、各プロファイリング期間における失われたサンプル数が正確にカウントされ、以前のように累積されて過剰に報告されることがなくなります。結果として、CPUプロファイリングデータの信頼性と正確性が大幅に向上しました。

これらの変更は、GoのCPUプロファイラがより正確なパフォーマンスデータを提供できるようになり、開発者がアプリケーションのボトルネックを特定する際に、より信頼性の高い情報に基づいた意思決定を行えるようになることを意味します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goのソースコード (特に src/runtime/ ディレクトリ)
  • 一般的なCPUプロファイリングの概念に関する知識
  • コミットメッセージと差分情報