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

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

このコミットは、Go 1.2 リリースにおいて、スタックフレームの正確なポインタ収集(precise collection of stack frames)を一時的に無効化する変更です。これは、コールサイト固有のポインタビットマップのコードが間に合わず、その代替として必要となるゼロ初期化のコストが高すぎたためです。この機能はGo 1.3で導入される予定でした。

コミット

commit 30ecb4cd05ac41805593c95bd7967b808e5f4ca5
Author: Russ Cox <rsc@golang.org>
Date:   Mon Sep 16 20:26:10 2013 -0400

    build: disable precise collection of stack frames
    
    The code for call site-specific pointer bitmaps was not ready in time,
    but the zeroing required without it is too expensive to use by default.
    We will have to wait for precise collection of stack frames until Go 1.3.
    
    The precise collection can be re-enabled by
    
            GOEXPERIMENT=precisestack ./all.bash
    
    but that will not be the default for a Go 1.2 build.
    
    Fixes #6087.
    
    R=golang-dev, jeremyjackins, dan.kortschak, r
    CC=golang-dev
    https://golang.org/cl/13677045

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

https://github.com/golang/go/commit/30ecb4cd05ac41805593c95bd7967b808e5f4ca5

元コミット内容

build: disable precise collection of stack frames

このコミットは、Go 1.2のビルドにおいて、スタックフレームの正確なポインタ収集機能を無効化することを目的としています。その理由は、コールサイト固有のポインタビットマップを生成するコードが開発期限に間に合わなかったためです。この機能がない場合、デフォルトで必要となるメモリのゼロ初期化のコストが非常に高く、パフォーマンスに悪影響を与えるため、デフォルトでは無効化されました。正確なスタックフレーム収集はGo 1.3まで延期されることになりました。

ただし、GOEXPERIMENT=precisestack ./all.bash コマンドを使用することで、この機能を明示的に有効にすることは可能です。しかし、これはGo 1.2ビルドのデフォルト設定ではありません。

この変更は、Issue #6087 を修正するものです。

変更の背景

Goのガベージコレクション(GC)は、メモリ管理において重要な役割を果たします。特に、スタック上のポインタを正確に識別することは、GCが正しく動作し、不要なメモリを解放するために不可欠です。

このコミットが行われた2013年当時、GoのGCは進化の途中にありました。特に、スタックフレーム内のポインタを「正確に」識別する機能(precise collection of stack frames)は、GCの効率と精度を向上させるための重要な目標でした。正確なGCとは、メモリ内のどの値がポインタであり、どれがそうでないかを正確に識別できるGCを指します。これにより、GCは到達可能なオブジェクトを正確に追跡し、誤って非ポインタデータをポインタと解釈してオブジェクトが解放されない「誤った保持(false retention)」を防ぐことができます。

しかし、このコミットの時点では、コールサイト(関数呼び出しが行われる場所)ごとにスタックフレーム内のポインタの位置を示すビットマップを生成するコードが、Go 1.2のリリーススケジュールに間に合いませんでした。このビットマップがない場合、GCはスタック上のすべてのメモリ領域をポインタとして扱う必要があり、これは「保守的なGC(conservative GC)」と呼ばれるアプローチに近くなります。保守的なGCでは、ポインタではないデータが偶然ポインタのように見える場合でも、それが指す先のオブジェクトを到達可能と判断してしまう可能性があります。これを避けるためには、スタック上のメモリを広範囲にゼロ初期化する必要があり、これが非常に高コストな操作となります。

このパフォーマンス上の懸念から、Go開発チームは、正確なスタックフレーム収集機能をGo 1.2のデフォルトから外し、Go 1.3での導入を目指すことを決定しました。このコミットは、その決定を反映し、デフォルトでこの機能を無効化するためのビルドシステムへの変更を加えています。

前提知識の解説

Goのガベージコレクション (GC)

Goのガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはや到達不可能になった(どの変数からも参照されなくなった)メモリ領域を自動的に解放する仕組みです。GoのGCは、並行(concurrent)かつ低遅延(low-latency)であることを目指して設計されており、プログラムの実行と並行して動作することで、アプリケーションの一時停止時間を最小限に抑えます。

正確なGC (Precise GC) と保守的なGC (Conservative GC)

  • 正確なGC (Precise GC): GCがメモリ内のどの値がポインタであり、どれがそうでないかを正確に識別できる方式です。これにより、GCは到達可能なオブジェクトを正確に追跡し、誤って非ポインタデータをポインタと解釈してオブジェクトが解放されない「誤った保持」を防ぎます。GoのGCは、基本的に正確なGCを目指しています。
  • 保守的なGC (Conservative GC): GCがメモリ内の値がポインタであるかどうかを確実に判断できない場合、その値をポインタであると仮定して処理する方式です。これは実装が容易ですが、実際にはポインタではない値がポインタのように見えることで、不要なオブジェクトが解放されずにメモリリークを引き起こす可能性があります。

スタックフレーム (Stack Frame)

関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどを格納するために、ゴルーチン(goroutine)のスタック上に割り当てられるメモリ領域です。Goのランタイムは、これらのスタックのサイズを動的に管理し、必要に応じて拡大・縮小させます。

スタックルート (Stack Roots)

ガベージコレクションのサイクル中、GCはまず「ルート(roots)」と呼ばれる既知の到達可能な値から追跡を開始します。これらのルートには、グローバル変数や、アクティブなゴルーチンのスタックフレーム上のローカル変数が含まれます。GCはこれらのルートから到達可能なすべてのオブジェクトを追跡し、到達不可能なオブジェクトを解放します。スタックフレーム内のポインタを正確に識別することは、これらのスタックルートを正確に特定するために重要です。

GOEXPERIMENT 環境変数

GOEXPERIMENT は、Goのビルドシステムで使用される環境変数で、開発中の実験的な機能を有効または無効にするために使用されます。この変数を設定することで、特定の機能がデフォルトで有効になっていない場合でも、開発者やテスターがその機能を試すことができます。このコミットの時点では、precisestack という実験的なフラグが導入され、スタックフレームの正確な収集を制御するために使用されました。

技術的詳細

このコミットの技術的詳細は、Goのビルドプロセスとランタイムにおけるメモリ管理の複雑さを示しています。

  1. コールサイト固有のポインタビットマップの欠如: GoのGCがスタックフレーム内のポインタを正確に識別するためには、各関数呼び出し(コールサイト)において、そのスタックフレーム内のどのオフセットにポインタが存在するかを示すメタデータ(ポインタビットマップ)が必要です。このビットマップは、コンパイラによって生成され、ランタイムがGC時に参照します。このコミットの時点では、このビットマップを効率的かつ正確に生成するコードがGo 1.2のリリースまでに完成していませんでした。

  2. ゼロ初期化のコスト: ポインタビットマップがない場合、GCはスタック上のメモリ領域がポインタを含むかどうかを確実に判断できません。この状況で「誤った保持」を防ぐためには、スタック上のすべてのローカル変数をゼロ初期化する必要があります。これにより、古いポインタ値が残存してGCが誤ったオブジェクトを保持することを防ぎます。しかし、このゼロ初期化は、特に大きなスタックフレームや多数の関数呼び出しがある場合に、顕著なパフォーマンスオーバーヘッドを引き起こします。コミットメッセージにある「the zeroing required without it is too expensive to use by default」という記述は、このコストがデフォルトで有効にするには高すぎたことを示しています。

  3. GOEXPERIMENT の導入と利用: この問題を解決するため、開発チームは「正確なスタックフレーム収集」機能をデフォルトで無効化しつつも、開発者がテストできるように GOEXPERIMENT 環境変数を通じて有効化できるメカニズムを導入しました。

    • src/cmd/dist/a.h, src/cmd/dist/build.c, src/cmd/dist/buildruntime.c の変更は、ビルドシステムに GOEXPERIMENT 環境変数の値を zaexperiment.h というヘッダーファイルに書き込む機能を追加しています。このヘッダーファイルは、ランタイムコードに GOEXPERIMENT の値を提供します。
    • src/cmd/gc/go.hsrc/cmd/gc/lex.c の変更は、Goコンパイラ(gc)に precisestack_enabled というフラグを導入し、GOEXPERIMENT=precisestack が設定されている場合にこのフラグが有効になるようにしています。
    • src/cmd/gc/pgen.csrc/cmd/gc/walk.c の変更は、この precisestack_enabled フラグに基づいて、スタック変数のゼロ初期化ロジックや戻り値のゼロ初期化ロジックを条件付きで適用するように修正しています。precisestack_enabled が有効な場合は、より厳密なゼロ初期化が行われる可能性があります。
    • src/pkg/runtime/mgc0.csrc/pkg/runtime/proc.c, src/pkg/runtime/runtime.h の変更は、Goランタイムが GOEXPERIMENT の値を取得し、runtime·precisestack というグローバル変数にその状態を反映させるようにしています。GCのロジック(scaninterfacedata など)は、この runtime·precisestack の値に基づいて動作を調整します。

このアプローチにより、Go 1.2では安定性とパフォーマンスを優先し、高コストなゼロ初期化を避けるために正確なスタックフレーム収集を無効化しつつ、Go 1.3での完全な実装に向けた準備を進めることができました。

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

このコミットでは、主にGoのビルドシステム (src/cmd/dist)、Goコンパイラ (src/cmd/gc)、およびGoランタイム (src/pkg/runtime) に変更が加えられています。

  • src/cmd/dist/a.h: ビルドシステムが zaexperiment.h を生成するための関数 mkzexperiment のプロトタイプ宣言を追加。
  • src/cmd/dist/build.c: ビルドシステムが zaexperiment.h をビルドターゲットとして認識し、mkzexperiment 関数を呼び出すように設定。
  • src/cmd/dist/buildruntime.c: GOEXPERIMENT 環境変数の値を読み取り、zaexperiment.h ファイルに #define GOEXPERIMENT "experiment string" の形式で書き込む mkzexperiment 関数を実装。また、mkzasm 関数内でも GOEXPERIMENT の値を zasm_GOOS_GOARCH.h に書き込むように変更。
  • src/cmd/gc/go.h: コンパイラ内部で使用されるグローバル変数 precisestack_enabled の宣言を追加。
  • src/cmd/gc/lex.c: GOEXPERIMENT 環境変数で precisestack が指定された場合に precisestack_enabled フラグを有効にするためのエントリを exper 配列に追加。
  • src/cmd/gc/pgen.c: allocauto 関数内で、precisestack_enabled が有効な場合にのみ、ローカル変数のゼロ初期化に関するループを条件付きで実行するように変更。
  • src/cmd/gc/walk.c: paramstoheap 関数内で、戻り値のゼロ初期化の条件に precisestack_enabled を追加。precisestack モードでは、GCが戻り値を常にライブであると仮定するため、常にゼロ初期化を行うように変更。
  • src/pkg/runtime/mgc0.c: scaninterfacedata 関数内で、runtime·precisestack が有効な場合にのみ特定のGCロジックを実行するように変更。
  • src/pkg/runtime/proc.c: runtime·schedinit 関数内で、haveexperiment("precisestack") の結果を runtime·precisestack グローバル変数に設定。また、zaexperiment.h をインクルードし、GOEXPERIMENT の値に基づいて特定の実験が有効かどうかをチェックする haveexperiment 関数を実装。
  • src/pkg/runtime/runtime.h: runtime·precisestack グローバル変数の外部宣言を追加。

コアとなるコードの解説

このコミットの核心は、Goのビルドプロセス、コンパイラ、およびランタイムが連携して、GOEXPERIMENT 環境変数を通じて「正確なスタックフレーム収集」機能を制御するメカニズムを導入した点にあります。

  1. GOEXPERIMENT のビルドシステムへの統合 (src/cmd/dist/buildruntime.c): mkzexperiment 関数は、GOEXPERIMENT 環境変数の値を読み取り、zaexperiment.h というCヘッダーファイルを生成します。このヘッダーファイルには、#define GOEXPERIMENT "experiment string" の形式で、GOEXPERIMENT の値が文字列リテラルとして埋め込まれます。これにより、GoランタイムのCコードから GOEXPERIMENT の値にアクセスできるようになります。 また、mkzasm 関数も同様に GOEXPERIMENT の値を zasm_GOOS_GOARCH.h に書き込むように変更されており、アセンブリコードからもこの情報が利用可能になります。

  2. コンパイラでのフラグ制御 (src/cmd/gc/lex.c, src/cmd/gc/go.h): src/cmd/gc/lex.cexper 配列に {"precisestack", &precisestack_enabled} が追加されたことで、Goコンパイラは GOEXPERIMENT=precisestack が設定されている場合に、内部フラグ precisestack_enabledtrue に設定します。このフラグは src/cmd/gc/go.h で宣言されています。

  3. コンパイラでのコード生成の調整 (src/cmd/gc/pgen.c, src/cmd/gc/walk.c):

    • src/cmd/gc/pgen.callocauto 関数は、ローカル変数のメモリ割り当てと初期化を担当します。このコミットでは、precisestack_enabledtrue の場合にのみ、ローカル変数をゼロ初期化するループが実行されるように変更されました。これは、正確なスタック収集が有効な場合、GCがポインタを正確に識別できるため、すべてのローカル変数をゼロ初期化する必要がない、あるいは異なるゼロ初期化戦略が適用されることを示唆しています。
    • src/cmd/gc/walk.cparamstoheap 関数は、関数の戻り値の処理に関連します。この変更では、precisestack_enabledtrue の場合、戻り値が常にライブであるとGCが仮定するため、常にゼロ初期化を行うように条件が追加されました。これは、GCが戻り値を正確に追跡するために必要な措置です。
  4. ランタイムでの実験フラグの利用 (src/pkg/runtime/proc.c, src/pkg/runtime/mgc0.c):

    • src/pkg/runtime/proc.c では、zaexperiment.h をインクルードすることで、ビルド時に埋め込まれた GOEXPERIMENT の値にアクセスします。haveexperiment 関数は、この埋め込まれた文字列を解析し、特定の実験(例: precisestack)が有効かどうかをチェックします。
    • runtime·schedinit 関数内で、haveexperiment("precisestack") の結果が runtime·precisestack というグローバルな bool 型変数に格納されます。
    • src/pkg/runtime/mgc0.cscaninterfacedata のようなGC関連の関数は、この runtime·precisestack の値に基づいて、GCの動作を調整します。例えば、runtime·precisestacktrue の場合にのみ、特定のポインタスキャンロジックが実行されるようになります。

これらの変更により、Go 1.2ではデフォルトで高コストなゼロ初期化を回避しつつ、開発者が実験的に正確なスタックフレーム収集を試せる柔軟なメカニズムが提供されました。

関連リンク

参考にした情報源リンク