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

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

このコミットは、Goランタイムのデバッグ機能である GODEBUG=allocfreetrace=1GODEBUG=gcdead=1 の挙動を調整し、改善することを目的としています。特に allocfreetrace については、ヒーププロファイリングとの結合を解消し、より正確で有用なスタックトレース情報を提供するように変更されました。また、gcdead については、デッドポインタの検出精度を向上させています。

コミット

commit 1ec4d5e9e775b2adcf7dd2e464a10854bad09803
Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 1 13:30:10 2014 -0400

    runtime: adjust GODEBUG=allocfreetrace=1 and GODEBUG=gcdead=1
    
    GODEBUG=allocfreetrace=1:
    
    The allocfreetrace=1 mode prints a stack trace for each block
    allocated and freed, and also a stack trace for each garbage collection.
    
    It was implemented by reusing the heap profiling support: if allocfreetrace=1
    then the heap profile was effectively running at 1 sample per 1 byte allocated
    (always sample). The stack being shown at allocation was the stack gathered
    for profiling, meaning it was derived only from the program counters and
    did not include information about function arguments or frame pointers.
    The stack being shown at free was the allocation stack, not the free stack.
    If you are generating this log, you can find the allocation stack yourself, but
    it can be useful to see exactly the sequence that led to freeing the block:
    was it the garbage collector or an explicit free? Now that the garbage collector
    runs on an m0 stack, the stack trace for the garbage collector was never interesting.
    
    Fix all these problems:
    
    1. Decouple allocfreetrace=1 from heap profiling.
    2. Print the standard goroutine stack traces instead of a custom format.
    3. Print the stack trace at time of allocation for an allocation,
       and print the stack trace at time of free (not the allocation trace again)
       for a free.
    4. Print all goroutine stacks at garbage collection. Having all the stacks
       means that you can see the exact point at which each goroutine was
       preempted, which is often useful for identifying liveness-related errors.
    
    GODEBUG=gcdead=1:
    
    This mode overwrites dead pointers with a poison value.
    Detect the poison value as an invalid pointer during collection,
    the same way that small integers are invalid pointers.
    
    LGTM=khr
    R=khr
    CC=golang-codereviews
    https://golang.org/cl/81670043

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

https://github.com/golang/go/commit/1ec4d5e9e775b2adcf7dd2e464a10854bad09803

元コミット内容

runtime: adjust GODEBUG=allocfreetrace=1 and GODEBUG=gcdead=1

GODEBUG=allocfreetrace=1:

The allocfreetrace=1 mode prints a stack trace for each block
allocated and freed, and also a stack trace for each garbage collection.

It was implemented by reusing the heap profiling support: if allocfreetrace=1
then the heap profile was effectively running at 1 sample per 1 byte allocated
(always sample). The stack being shown at allocation was the stack gathered
for profiling, meaning it was derived only from the program counters and
did not include information about function arguments or frame pointers.
The stack being shown at free was the allocation stack, not the free stack.
If you are generating this log, you can find the allocation stack yourself, but
it can be useful to see exactly the sequence that led to freeing the block:
was it the garbage collector or an explicit free? Now that the garbage collector
runs on an m0 stack, the stack trace for the garbage collector was never interesting.

Fix all these problems:

1. Decouple allocfreetrace=1 from heap profiling.
2. Print the standard goroutine stack traces instead of a custom format.
3. Print the stack trace at time of allocation for an allocation,
   and print the stack trace at time of free (not the allocation trace again)
   for a free.
4. Print all goroutine stacks at garbage collection. Having all the stacks
   means that you can see the exact point at which each goroutine was
   preempted, which is often useful for identifying liveness-related errors.

GODEBUG=gcdead=1:

This mode overwrites dead pointers with a poison value.
Detect the poison value as an invalid pointer during collection,
the same way that small integers are invalid pointers.

LGTM=khr
R=khr
CC=golang-codereviews
https://golang.org/cl/81670043

変更の背景

このコミットが行われた背景には、Goランタイムのデバッグ機能、特にメモリ割り当てと解放の追跡 (allocfreetrace) およびガベージコレクション (GC) のデバッグ (gcdead) における既存の課題がありました。

  1. allocfreetrace=1 の問題点:

    • ヒーププロファイリングとの結合: 以前の allocfreetrace=1 は、ヒーププロファイリングのメカニズムを再利用していました。これにより、allocfreetrace=1 を有効にすると、実質的にヒーププロファイリングが「常にサンプリング」される状態になり、オーバーヘッドが大きくなる可能性がありました。
    • スタックトレースの品質: 割り当て時に表示されるスタックトレースは、プロファイリングのために収集されたものであり、プログラムカウンタのみから導出されていました。そのため、関数引数やフレームポインタに関する情報が含まれておらず、デバッグの際に十分な情報を提供できませんでした。
    • 解放時のスタックトレースの不正確さ: メモリ解放時に表示されるスタックトレースは、解放時のものではなく、割り当て時のスタックトレースが再利用されていました。これにより、実際にメモリが解放された経緯(GCによるものか、明示的な解放かなど)を追跡することが困難でした。
    • GC時のスタックトレースの有用性の欠如: GCが m0 スタックで実行されるようになったため、GC時のスタックトレースはほとんど有用な情報を提供していませんでした。GCがどのゴルーチンをプリエンプトしたかなど、並行処理におけるライブネス関連のエラーを特定する上で重要な情報が不足していました。
  2. gcdead=1 の改善:

    • gcdead=1 はデッドポインタを特定の「ポイズン値」で上書きする機能ですが、このポイズン値がGC中に無効なポインタとして正しく検出されるように、検出ロジックの改善が必要でした。

これらの問題に対処し、Goランタイムのデバッグ機能をより強力で使いやすいものにすることが、このコミットの主要な動機となりました。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムおよびデバッグに関する概念を理解しておく必要があります。

  • GODEBUG 環境変数: Goプログラムの実行時にランタイムの挙動を制御するための環境変数です。様々なデバッグオプションやパフォーマンスチューニングオプションを提供します。例えば、GODEBUG=gctrace=1 はGCのトレース情報を出力します。
  • allocfreetrace: GODEBUG のオプションの一つで、メモリの割り当て (allocation) と解放 (free) のイベントをトレースし、関連するスタックトレースを出力する機能です。メモリリークの特定や、メモリ使用パターンの分析に役立ちます。
  • gcdead: GODEBUG のオプションの一つで、ガベージコレクションによって「デッド」と判断されたポインタ(参照されなくなったメモリ領域へのポインタ)を特定の「ポイズン値」で上書きする機能です。これにより、デッドポインタが誤って使用された場合に、その不正なアクセスを早期に検出することができます。
  • ヒーププロファイリング: Goの pprof ツールなどで提供される機能で、プログラムがヒープメモリをどのように使用しているかを分析するためのものです。メモリ割り当ての頻度、サイズ、場所などを統計的に収集し、メモリリークや過剰なメモリ使用を特定するのに役立ちます。
  • スタックトレース: プログラムの実行中に、ある時点での関数呼び出しの連鎖(コールスタック)を示す情報です。デバッグ時に、エラーが発生した場所や、特定の関数がどのように呼び出されたかを特定するために不可欠です。
  • ゴルーチン (Goroutine): Goの軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行できます。
  • m0 スタック: Goランタイム内部で使用される特別なスタックの一つです。ガベージコレクタなどのランタイムの重要な処理は、ユーザーゴルーチンのスタックとは異なる m0 スタックで実行されることがあります。
  • ポイズン値 (Poison Value): デバッグ目的で、無効なデータや解放されたメモリ領域に書き込まれる特定のパターン値です。これにより、その領域が誤ってアクセスされた場合に、その値が異常であることを検出できます。このコミットでは 0x6969696969696969LL という値が PoisonPtr として定義されています。

技術的詳細

このコミットは、前述の問題点を解決するために、以下の技術的な変更を導入しています。

  1. allocfreetrace=1 とヒーププロファイリングの分離:

    • 以前は allocfreetrace=1 が有効な場合、ヒーププロファイリングが常にサンプリングされるように動作していました。このコミットでは、allocfreetrace のロジックをヒーププロファイリング (MProf_Malloc, MProf_Free) から分離し、runtime·traceallocruntime·tracefreeruntime·tracegc という新しい関数を導入しました。これにより、allocfreetrace は独立して動作し、ヒーププロファイリングのオーバーヘッドなしにトレース情報を出力できるようになりました。
    • runtime/malloc.gocruntime·mallocgc 関数内で、runtime·debug.allocfreetrace が真の場合に runtime·tracealloc を直接呼び出すように変更されています。また、profilealloc の呼び出しパスから typ 引数が削除され、MProf_Malloc も同様に変更されています。
  2. 標準ゴルーチンスタックトレースの利用:

    • 以前の allocfreetrace はカスタムフォーマットでスタックトレースを出力していましたが、このコミットでは runtime·traceback 関数を利用して、標準のゴルーチンスタックトレースを出力するように変更されました。これにより、より詳細で理解しやすいスタック情報(関数引数やフレームポインタを含む可能性)が提供されます。
    • runtime/mprof.goc から printstackframes 関数が削除され、代わりに runtime·traceallocruntime·tracefreeruntime·tracegc 内で runtime·tracebackruntime·tracebackothers が使用されています。
  3. 割り当て時と解放時の正確なスタックトレース:

    • 割り当て時には runtime·tracealloc が呼び出され、その時点でのスタックトレースが出力されます。
    • 解放時には runtime·tracefree が呼び出され、その時点でのスタックトレースが出力されます。これにより、メモリがGCによって解放されたのか、あるいは明示的な free 呼び出しによって解放されたのかを正確に区別できるようになりました。
    • runtime/malloc.gocruntime·free 関数に runtime·tracefree の呼び出しが追加され、runtime/mgc0.cruntime·MSpan_Sweep 関数内でも runtime·tracefree が呼び出されるようになりました。
  4. GC時の全ゴルーチンスタックトレースの出力:

    • GCが実行される際に、runtime·tracegc が呼び出され、その時点で実行中の全てのゴルーチンのスタックトレースが出力されるようになりました。これは、特に並行処理において、GCがどのゴルーチンをどの時点でプリエンプトしたかを把握する上で非常に有用です。ライブネス関連のバグ(例えば、あるゴルーチンがGCによって一時停止され、その間に別のゴルーチンが予期せぬ状態になった場合など)の特定に役立ちます。
    • runtime/mgc0.cruntime·gc 関数内で runtime·tracegc が呼び出されるようになりました。また、m->traceback = 2 の設定により、トレースバックの深度が調整されています。
  5. gcdead=1 のポイズン値検出の改善:

    • gcdead=1 が有効な場合、デッドポインタを PoisonPtr (値 0x6969696969696969LL) で上書きします。このコミットでは、GC中のポインタスキャンにおいて、この PoisonPtr が無効なポインタとして正しく検出されるようにロジックが追加されました。これは、小さな整数値が無効なポインタとして扱われるのと同様のメカニズムです。
    • runtime/malloc.hPoisonPtr が定義され、runtime/mgc0.cscanbitvector 関数内で、ポインタが PoisonPtr と等しい場合に無効なポインタとして扱う条件が追加されています。

これらの変更により、Goランタイムのデバッグ機能は大幅に強化され、開発者がメモリ関連の問題や並行処理のバグをより効率的に診断できるようになりました。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/pkg/runtime/malloc.goc:
    • profilealloc 関数のシグネチャから typ 引数が削除されました。
    • runtime·mallocgc 内で runtime·debug.allocfreetrace が有効な場合に profilealloc の代わりに runtime·tracealloc を呼び出すように変更されました。
    • runtime·free 関数に runtime·tracefree の呼び出しが追加されました。
  • src/pkg/runtime/malloc.h:
    • runtime·tracealloc, runtime·tracefree, runtime·tracegc のプロトタイプ宣言が追加されました。
    • runtime·MProf_Mallocruntime·MProf_Free のシグネチャが変更され、typvoid *p 引数が削除されました。
    • runtime·MProf_TraceGC の宣言が削除されました。
    • PoisonPtr マクロが定義されました。
  • src/pkg/runtime/mgc0.c:
    • scanbitvector 関数内で、runtime·debug.gcdead が有効な場合に PoisonPtr を使用してデッドポインタを上書きするように変更されました。また、ポインタが PoisonPtr と等しい場合を無効なポインタとして検出するロジックが追加されました。
    • runtime·MSpan_Sweep 関数内で、runtime·debug.allocfreetrace が有効な場合に runtime·tracefree を呼び出すように変更されました。
    • runtime·gchelper および runtime·gc 関数内で m->traceback = 2 の設定と runtime·tracegc の呼び出しが追加されました。
  • src/pkg/runtime/mheap.c:
    • runtime·MProf_Free の呼び出し箇所で引数が変更されました。
  • src/pkg/runtime/mprof.goc:
    • typeinfonameprintstackframesruntime·MProf_TraceGC 関数が削除されました。
    • runtime·MProf_Malloc および runtime·MProf_Free から allocfreetrace 関連のロジック(runtime·printfprintstackframes の呼び出し)が削除され、シグネチャも変更されました。
    • runtime·traceallocruntime·tracefreeruntime·tracegc の新しい実装が追加されました。これらの関数は、tracelock を使用して排他制御を行い、runtime·printfruntime·traceback を利用して詳細なトレース情報を出力します。
  • src/pkg/runtime/stack.c:
    • adjustpointers 関数内で、ポインタが PoisonPtr と等しい場合を無効なポインタとして検出する条件が追加されました。

コアとなるコードの解説

このコミットの核心は、allocfreetrace の機能をヒーププロファイリングから完全に分離し、より詳細で正確なスタックトレース情報を提供する新しいトレース関数群を導入した点にあります。

  • runtime·tracealloc(void *p, uintptr size, uintptr typ):

    • メモリが割り当てられた直後に呼び出されます。
    • 割り当てられたメモリのアドレス p、サイズ size、型情報 typ を引数に取ります。
    • tracelock を取得して排他制御を行い、複数のゴルーチンからの同時呼び出しによる出力の混在を防ぎます。
    • m->traceback = 2 を設定することで、スタックトレースの深度を調整し、より詳細な情報を取得できるようにします。
    • runtime·printf を使用して、割り当てられたオブジェクトのアドレス、サイズ、型情報を出力します。
    • runtime·goroutineheader(g)runtime·traceback(...) を呼び出すことで、現在のゴルーチンのヘッダ情報と、割り当てが行われた時点の正確なスタックトレースを出力します。これにより、どのコードパスがメモリ割り当てを引き起こしたかを明確に把握できます。
  • runtime·tracefree(void *p, uintptr size):

    • メモリが解放された直後に呼び出されます。これは、明示的な free 呼び出しによるものか、GCによるものかに関わらず呼び出されます。
    • 解放されたメモリのアドレス p とサイズ size を引数に取ります。
    • tracelock を取得し、m->traceback = 2 を設定します。
    • runtime·printf で解放されたオブジェクトのアドレスとサイズを出力します。
    • runtime·goroutineheader(g)runtime·traceback(...) を呼び出すことで、解放が行われた時点の正確なスタックトレースを出力します。これにより、メモリがどのように、そしてどのコードパスによって解放されたかを追跡できます。
  • runtime·tracegc(void):

    • ガベージコレクションが開始される際に呼び出されます。
    • tracelock を取得し、m->traceback = 2 を設定します。
    • runtime·printf("tracegc()\\n") を出力し、GCの開始を示します。
    • runtime·tracebackothers(g) を呼び出す点が重要です。これは、GCを実行しているゴルーチン(通常は g0 スタック)以外の、全てのユーザーゴルーチンのスタックトレースを出力します。これにより、GCが実行された瞬間に各ゴルーチンがどのような状態にあったか、どこでプリエンプトされたかといった情報が得られ、並行処理におけるライブネス問題のデバッグに非常に役立ちます。
    • GCトレースの終了を示す end tracegc と改行を出力します。
  • PoisonPtr の導入と検出ロジック:

    • #define PoisonPtr ((uintptr)0x6969696969696969LL) により、デッドポインタを上書きするための特定の64ビット値が定義されました。
    • src/pkg/runtime/mgc0.cscanbitvector 関数では、GC中にポインタをスキャンする際、precise モードでポインタが PageSize 未満であるか、または PoisonPtr と等しい場合に、それを無効なポインタとして検出するロジックが追加されました。これにより、gcdead=1 が有効な場合に、デッドポインタが誤って参照された際に、ランタイムがその不正を検出しやすくなります。

これらの変更は、Goランタイムのデバッグ能力を向上させ、開発者がメモリ管理や並行処理に関連する複雑な問題をより深く理解し、解決するための強力なツールを提供します。

関連リンク

参考にした情報源リンク