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

[インデックス 15278] ファイルの概要

このコミットは、Go言語のプロファイリングツールである runtime/pprof において、報告されるスタックトレースの行番号が、実際の呼び出しサイト(call site)ではなく、呼び出し命令の次の命令を指してしまう問題を修正するものです。これにより、プロファイル結果がより正確で理解しやすくなります。

コミット

commit 318309a51f40d31568bd2c9131d1cd25c2ca0214
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 15 14:27:16 2013 -0500

    runtime/pprof: adjust reported line numbers to show call sites
    
    This is the same logic used in the standard tracebacks.
    The caller pc is the pc after the call, so except in the
    fake "call" caused by a panic, back up the pc enough
    that the lookup will use the previous instruction.
    
    Fixes #4150.
    Fixes #4151.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/7317047

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/318309a51f40d31568bd2c9131d1cd25c2ca0214

元コミット内容

runtime/pprof: adjust reported line numbers to show call sites

This is the same logic used in the standard tracebacks.
The caller pc is the pc after the call, so except in the
fake "call" caused by a panic, back up the pc enough
that the lookup will use the previous instruction.

Fixes #4150.
Fixes #4151.

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7317047

変更の背景

この変更は、Go言語のプロファイリングツール pprof が生成するスタックトレースにおいて、報告されるソースコードの行番号が直感的ではないという問題に対処するために行われました。具体的には、関数呼び出しが行われた場所(呼び出しサイト)ではなく、呼び出し命令の直後の命令の行番号が報告されてしまうという挙動がありました。

この問題は、以下の2つのIssueで報告されていました。

  • Issue #4150: pprof reports wrong line numbers for call sites このIssueでは、pprof が出力するスタックトレースの行番号が、関数呼び出しが行われた行ではなく、呼び出し後の行を指していることが指摘されていました。これにより、プロファイル結果を基にパフォーマンスボトルネックを特定する際に混乱が生じていました。

  • Issue #4151: pprof reports wrong line numbers for call sites (panic) このIssueは、パニック発生時のスタックトレースにおいても同様の問題が発生していることを報告していました。パニックは通常の関数呼び出しとは異なるメカニズムで処理されるため、その特殊性も考慮する必要がありました。

これらの問題は、Goの標準的なスタックトレース(例えば、パニック発生時に出力されるもの)が既に呼び出しサイトを正確に報告しているのに対し、pprof が異なる挙動を示すことで、ユーザー体験の一貫性を損ねていました。このコミットは、pprof のスタックトレースも標準のトレースバックと同じロジックを適用することで、この不整合を解消し、より正確で有用なプロファイル情報を提供することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語のランタイム、プロファイリング、およびCPUアーキテクチャに関する基本的な知識が必要です。

  • runtime/pprof: Go言語に組み込まれているプロファイリングパッケージです。CPU使用率、メモリ割り当て、ゴルーチンブロックなど、様々なプロファイル情報を収集し、可視化するためのツールを提供します。pprof は、プログラムの実行中に特定のイベント(例: 関数呼び出し、メモリ割り当て)が発生した際のスタックトレースを記録し、どの関数がどれだけの時間やリソースを消費しているかを分析するのに役立ちます。

  • スタックトレース (Stack Trace): プログラムの実行中に、ある時点での関数呼び出しの連鎖(コールスタック)を記録したものです。各エントリは、関数名、ファイル名、行番号、およびプログラムカウンタ(PC)を含みます。デバッグやプロファイリングにおいて、コードのどの部分で問題が発生しているか、あるいはリソースが消費されているかを特定するために不可欠です。

  • プログラムカウンタ (Program Counter, PC): CPUのレジスタの一つで、次に実行される命令のアドレスを保持しています。関数呼び出し命令が実行されると、PCは呼び出された関数の先頭アドレスにジャンプします。呼び出し元に戻る際には、呼び出し命令の次の命令のアドレスがPCに設定されます。

  • runtime.FuncForPC(pc uintptr) *Func: Goのランタイムパッケージ runtime に含まれる関数で、与えられたプログラムカウンタ pc に対応する関数情報を取得します。この関数は、スタックトレースをシンボリックに解決するために使用されます。

  • Func.FileLine(pc uintptr) (file string, line int): runtime.FuncForPC が返す Func 型のメソッドで、与えられたプログラムカウンタ pc に対応するソースファイル名と行番号を返します。

  • Func.Entry() uintptr: Func 型のメソッドで、関数のエントリポイント(関数の最初の命令のアドレス)を返します。

  • 関数呼び出しとPC: 一般的なCPUアーキテクチャでは、関数呼び出し命令(例: CALL 命令)が実行されると、リターンアドレス(呼び出し命令の次の命令のアドレス)がスタックにプッシュされ、PCは呼び出される関数のエントリポイントに設定されます。関数から戻る際には、スタックからリターンアドレスがポップされ、PCに設定されます。したがって、runtime.FuncForPC(pc)Func.FileLine(pc) に渡される pc は、通常、呼び出し命令の「次の」命令のアドレスを指します。

  • パニック (Panic): Go言語におけるランタイムエラーの一種です。パニックが発生すると、通常の実行フローは中断され、遅延関数(defer)が実行され、最終的にプログラムがクラッシュするか、recover によってパニックが捕捉されない限り、スタックトレースが出力されます。パニックは通常の関数呼び出しとは異なるメカニズムで処理されるため、スタックトレースの生成においても特別な考慮が必要になる場合があります。

  • CPUアーキテクチャ (386, amd64, armなど): CPUの設計思想や命令セットの違いを指します。異なるアーキテクチャでは、命令の長さやエンコーディングが異なるため、PCの調整方法も変わる可能性があります。例えば、386amd64 (x86系) では可変長命令が一般的ですが、arm などでは固定長命令が一般的です。この違いが、PCを「戻す」際のオフセット値に影響します。

技術的詳細

このコミットの核心は、runtime/pprof パッケージ内の printStackRecord 関数におけるプログラムカウンタ(PC)の調整ロジックにあります。

問題点: 従来の pprof のスタックトレース生成では、runtime.FuncForPC(pc)f.FileLine(pc) に渡される pc は、関数呼び出し命令の「次の」命令のアドレスをそのまま使用していました。これにより、プロファイル結果に表示される行番号が、実際にその関数が呼び出されたソースコード上の行(呼び出しサイト)ではなく、呼び出し命令の直後の行を指してしまい、プロファイリング情報の解釈を困難にしていました。

解決策: このコミットでは、Goの標準的なスタックトレース(例えば、パニック時に出力されるもの)が既に採用しているロジックを pprof にも適用します。このロジックは、pc が関数呼び出しの直後の命令を指していることを考慮し、pc を適切な量だけ「巻き戻す」ことで、f.FileLine が呼び出しサイトの正確なファイル名と行番号を返すようにします。

具体的な調整ロジック:

  1. tracepc 変数の導入: 各スタックフレームのPC pc を直接使用する代わりに、tracepc という新しい変数を導入します。この tracepcf.FileLine に渡される実際のPCとなります。

  2. PCの巻き戻し条件:

    • i > 0: スタックトレースの最初のフレーム(現在の関数)ではない場合。最初のフレームは呼び出し元ではないため、PCの調整は不要です。
    • pc > f.Entry(): 現在のPCが関数のエントリポイントよりも大きい場合。これは、PCが関数内のどこかを指していることを意味します。
    • !wasPanic: パニックによる「偽の呼び出し」ではない場合。パニックは通常の関数呼び出しとは異なるため、PCの調整ロジックも異なります。パニックの場合、runtime.panic 関数が呼び出されるため、その後のPCは既に適切な場所を指している可能性があります。
  3. アーキテクチャごとのPC調整:

    • runtime.GOARCH == "386" || runtime.GOARCH == "amd64": x86およびx86-64アーキテクチャの場合、tracepc を1バイト減らします (tracepc--)。これは、x86系の命令が可変長であり、関数呼び出し命令の直前の命令の開始アドレスを特定するために、一般的に1バイト戻すことで十分な場合が多いことを示唆しています。
    • else: それ以外のアーキテクチャ(例: arm)の場合、tracepc を4バイト減らします (tracepc -= 4)。これは、ARMなどのRISCアーキテクチャでは命令が固定長(通常4バイト)であり、関数呼び出し命令の開始アドレスを特定するために4バイト戻す必要があることを示唆しています。
  4. パニックの特殊処理:

    • wasPanic = name == "runtime.panic": 現在の関数名が runtime.panic である場合、wasPanic フラグを true に設定します。これにより、次のスタックフレーム(パニックを引き起こした呼び出し元)のPC調整時に、パニックの特殊な挙動が考慮されます。

この調整により、f.FileLine(tracepc) は、関数が実際に呼び出されたソースコード上の正確なファイル名と行番号を返すようになり、pprof の出力がより直感的で有用なものになります。

コアとなるコードの変更箇所

--- a/src/pkg/runtime/pprof/pprof.go
+++ b/src/pkg/runtime/pprof/pprof.go
@@ -318,21 +318,33 @@ func printCountProfile(w io.Writer, debug int, name string, p countProfile) erro
 // for a single stack trace.
 func printStackRecord(w io.Writer, stk []uintptr, allFrames bool) {
 	show := allFrames
-	for _, pc := range stk {
+	wasPanic := false
+	for i, pc := range stk {
 		f := runtime.FuncForPC(pc)
 		if f == nil {
 			show = true
 			fmt.Fprintf(w, "#\t%#x\n", pc)
+			wasPanic = false
 		} else {
-			file, line := f.FileLine(pc)
+			tracepc := pc
+			// Back up to call instruction.
+			if i > 0 && pc > f.Entry() && !wasPanic {
+				if runtime.GOARCH == "386" || runtime.GOARCH == "amd64" {
+					tracepc--
+				} else {
+					tracepc -= 4 // arm, etc
+				}
+			}
+			file, line := f.FileLine(tracepc)
 			name := f.Name()
 			// Hide runtime.goexit and any runtime functions at the beginning.
 			// This is useful mainly for allocation traces.
+			wasPanic = name == "runtime.panic"
 			if name == "runtime.goexit" || !show && strings.HasPrefix(name, "runtime.") {
 				continue
 			}
 			show = true
-			fmt.Fprintf(w, "#\t%#x\t%s+%#x\t%s:%d\n", pc, f.Name(), pc-f.Entry(), file, line)
+			fmt.Fprintf(w, "#\t%#x\t%s+%#x\t%s:%d\n", pc, name, pc-f.Entry(), file, line)
 		}
 	}
 	if !show {

コアとなるコードの解説

変更は src/pkg/runtime/pprof/pprof.go ファイルの printStackRecord 関数に集中しています。この関数は、単一のスタックトレースを整形して出力する役割を担っています。

  1. wasPanic 変数の導入:

    	wasPanic := false
    

    スタックトレースを逆順に処理する際に、前のフレームがパニックによって呼び出されたものかどうかを追跡するためのブーリアン変数 wasPanic が導入されました。これは、パニックが通常の関数呼び出しとは異なるスタックフレームの生成メカニズムを持つため、PCの調整ロジックに影響を与える可能性があるためです。

  2. ループ変数の変更:

    -	for _, pc := range stk {
    +	for i, pc := range stk {
    

    スタックフレームのインデックス i がループに追加されました。これにより、現在のフレームがスタックトレースの最初のフレーム(i == 0)であるかどうかを判断できるようになります。最初のフレームは現在の実行ポイントであり、呼び出し元ではないため、PCの調整は不要です。

  3. f == nil ブロック内の変更:

    		if f == nil {
    			show = true
    			fmt.Fprintf(w, "#\t%#x\n", pc)
    +			wasPanic = false
    		} else {
    

    runtime.FuncForPC(pc)nil を返す(つまり、PCに対応する関数情報が見つからない)場合、wasPanicfalse にリセットしています。これは、不明なPCの後に続くフレームがパニックに関連している可能性がないことを保証するためです。

  4. tracepc の導入とPC調整ロジック:

    +			tracepc := pc
    +			// Back up to call instruction.
    +			if i > 0 && pc > f.Entry() && !wasPanic {
    +				if runtime.GOARCH == "386" || runtime.GOARCH == "amd64" {
    +					tracepc--
    +				} else {
    +					tracepc -= 4 // arm, etc
    +				}
    +			}
    +			file, line := f.FileLine(tracepc)
    
    • tracepc := pc: まず、元のPC pctracepc にコピーします。この tracepcf.FileLine に渡されることになります。
    • if i > 0 && pc > f.Entry() && !wasPanic: PCを調整するかどうかの条件分岐です。
      • i > 0: 現在のフレームがスタックトレースの最初のフレームではないことを確認します。
      • pc > f.Entry(): 現在のPCが関数のエントリポイントよりも大きいことを確認します。これにより、PCが関数内の有効な命令を指していることを保証します。
      • !wasPanic: 前のフレームがパニックによるものではないことを確認します。
    • if runtime.GOARCH == "386" || runtime.GOARCH == "amd64":
      • tracepc--: x86およびx86-64アーキテクチャの場合、tracepc を1減らします。これは、関数呼び出し命令の直前の命令の開始アドレスに近づけるためです。
    • else:
      • tracepc -= 4: それ以外のアーキテクチャ(例: ARM)の場合、tracepc を4減らします。これは、固定長命令セットを持つアーキテクチャで、関数呼び出し命令の開始アドレスに正確に戻るためです。
    • file, line := f.FileLine(tracepc): 調整された tracepc を使用して、正確なファイル名と行番号を取得します。
  5. wasPanic の更新:

    +			wasPanic = name == "runtime.panic"
    

    現在の関数名が runtime.panic である場合、wasPanic フラグを true に設定します。これにより、次のスタックフレームの処理で、パニックの特殊な挙動が考慮されます。

  6. fmt.Fprintf の変更:

    -			fmt.Fprintf(w, "#\t%#x\t%s+%#x\t%s:%d\n", pc, f.Name(), pc-f.Entry(), file, line)
    +			fmt.Fprintf(w, "#\t%#x\t%s+%#x\t%s:%d\n", pc, name, pc-f.Entry(), file, line)
    

    f.Name()name に変更していますが、これは機能的な変更ではなく、既に name := f.Name() で取得されている変数を使用する形に統一したものです。重要なのは、fileline が調整された tracepc から取得された値になっている点です。

これらの変更により、pprof が出力するスタックトレースの行番号が、より正確に呼び出しサイトを指すようになり、プロファイル結果の解釈性が大幅に向上しました。

関連リンク

参考にした情報源リンク