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

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

このコミットは、Goコンパイラ(cmd/gc)とランタイム(runtime)において、precisestack機能をデフォルトで有効にする変更を導入します。precisestackは、ガベージコレクタがスタックをスキャンする際の精度を向上させ、未初期化のスタック領域をポインタとして誤認識する問題を解消することを目的としています。

コミット

commit 53061193f1b35aa6eda405909db41900fdc2c5de
Author: Russ Cox <rsc@golang.org>
Date:   Wed Feb 19 17:09:08 2014 -0500

    cmd/gc, runtime: enable precisestack by default
    
    [Repeat of CL 64100044, after 32-bit fix in CL 66170043.]
    
    Precisestack makes stack collection completely precise,
    in the sense that there are no "used and not set" errors
    in the collection of stack frames, no times where the collector
    reads a pointer from a stack word that has not actually been
    initialized with a pointer (possibly a nil pointer) in that function.
    
    The most important part is interfaces: precisestack means
    that if reading an interface value, the interface value is guaranteed
    to be initialized, meaning that the type word can be relied
    upon to be either nil or a valid interface type word describing
    the data word.
    
    This requires additional zeroing of certain values on the stack
    on entry, which right now costs about 5% overall execution
    time in all.bash. That cost will come down before Go 1.3
    (issue 7345).
    
    There are at least two known garbage collector bugs right now,
    issues 7343 and 7344. The first happens even without precisestack.
    The second I have only seen with precisestack, but that does not
    mean that precisestack is what causes it. In fact it is very difficult
    to explain by what precisestack does directly. Precisestack may
    be exacerbating an existing problem. Both of those issues are
    marked for Go 1.3 as well.
    
    The reasons for enabling precisestack now are to give it more
    time to soak and because the copying stack work depends on it.
    
    LGTM=r
    R=r
    CC=golang-codereviews
    https://golang.org/cl/65820044

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

https://github.com/golang/go/commit/53061193f1b35aa6eda405909db41900fdc2c5de

元コミット内容

このコミットは、以前の変更セット(CL 64100044)の繰り返しであり、32ビット環境での修正(CL 66170043)が適用された後のものです。これは、precisestack機能の導入が以前にも試みられたが、何らかの問題(特に32ビット環境での問題)により一度ロールバックされたか、あるいはデフォルト有効化が見送られていたことを示唆しています。今回のコミットで、その問題が解決されたため、改めてデフォルト有効化が試みられています。

変更の背景

Goのガベージコレクタ(GC)は、プログラムが使用しているメモリを正確に識別し、不要になったメモリを解放する役割を担っています。このプロセスにおいて、スタック上に存在するポインタを正確に識別することは非常に重要です。しかし、従来のGCでは、スタック上のメモリがまだ初期化されていないにもかかわらず、そこにたまたまポインタのように見える値が存在した場合、GCがそれを有効なポインタと誤認識してしまう可能性がありました。これを「used and not set」エラーと呼びます。

特に、インターフェース値の扱いにおいてこの問題は顕著でした。Goのインターフェースは、内部的に型情報とデータポインタのペアとして表現されます。もしインターフェース変数が初期化される前にGCがスタックをスキャンした場合、型情報が未初期化であるために、GCが不正なメモリを指していると判断したり、クラッシュを引き起こしたりする可能性がありました。

precisestackは、この問題を根本的に解決するために導入されました。スタック上のポインタを含む可能性のあるすべての場所が、関数エントリ時に確実に初期化(ゼロクリアまたは有効な値で設定)されるようにすることで、GCが常に正確な情報を得られるようにします。

この変更がGo 1.3のリリース前にデフォルトで有効化される背景には、以下の理由があります。

  1. 十分なテスト期間の確保(Soak Time): 新しいGCの挙動は、様々な実際のアプリケーションでテストされることで、潜在的なバグが発見されやすくなります。Go 1.3のリリース前にデフォルトで有効にすることで、より多くのユーザーがこの機能を使い、フィードバックやバグ報告を促すことができます。
  2. 将来のGC機能への依存: コミットメッセージには「copying stack work depends on it」と明記されています。これは、GoのGCが将来的にスタックをコピーする方式(おそらく、より低レイテンシなGCを実現するため)に移行する計画があり、その実現にはprecisestackによるスタックの正確なスキャンが不可欠であることを示しています。

前提知識の解説

ガベージコレクション (GC) とスタックスキャン

Goのガベージコレクタは、マーク&スイープ方式をベースとしています。GCは、プログラムが現在使用しているオブジェクト(到達可能なオブジェクト)を特定するために、ルートセット(グローバル変数、レジスタ、そしてスタック上のポインタ)からメモリグラフを辿ります。

スタックは、関数呼び出しの際にローカル変数や引数、戻りアドレスなどが一時的に格納されるメモリ領域です。GoのGCがスタックをスキャンする際、スタック上のどのワードがポインタであり、どのワードが単なる整数やその他の非ポインタデータであるかを正確に識別する必要があります。

ポインタの精度 (Precise vs. Conservative)

  • Precise GC (正確なGC): メモリ上のどの場所がポインタであるかを正確に識別できるGCです。これにより、GCは到達可能なオブジェクトのみをマークし、不要なメモリを正確に解放できます。GoのGCは、ヒープ上のオブジェクトに対しては基本的に正確なGCを行います。
  • Conservative GC (保守的なGC): メモリ上の特定のワードがポインタであるかどうかを確実に判断できない場合、それをポインタであると仮定するGCです。これは、誤ってポインタでないものをポインタとみなしてしまうリスクがありますが、その代わりにGCの実装が簡素化されます。スタックスキャンにおいては、一部のGCが保守的なアプローチを取ることがあります。GoのGCも、precisestack導入以前はスタックの一部で保守的なスキャンを行っていた可能性があります。

「Used and not set」エラー

これは、プログラムがまだ値を書き込んでいないメモリ領域(特にスタック上のローカル変数など)を、GCがポインタとして読み取ってしまう問題です。未初期化のメモリには、以前のプログラムの実行で残されたゴミデータが含まれている可能性があり、それがたまたま有効なメモリアドレスのように見えてしまうことがあります。GCがこれをポインタと誤認識すると、存在しないオブジェクトをマークしようとしたり、不正なメモリにアクセスしてクラッシュしたり、あるいは本来解放されるべきメモリを誤って保持し続けてメモリリークを引き起こしたりする可能性があります。

インターフェース値の内部表現

Goのインターフェース値は、内部的に2つのワードで構成されます。

  1. 型ワード (Type Word): インターフェースが保持している具体的な値の型情報(_type構造体へのポインタ)。
  2. データワード (Data Word): インターフェースが保持している具体的な値へのポインタ。

インターフェース変数が宣言された直後でまだ値が代入されていない場合、これら2つのワードは未初期化の状態です。precisestackがなければ、GCがこの未初期化のインターフェース変数をスキャンした際に、型ワードやデータワードにゴミデータが残っていると、それを有効な型情報やデータポインタと誤認識するリスクがありました。precisestackは、インターフェース変数が初期化されることを保証することで、この問題を解決します。

Goの実験的機能 (GOEXPERIMENT)

Goコンパイラやランタイムには、開発中の機能や実験的な機能を有効にするためのGOEXPERIMENT環境変数があります。この変数を設定することで、特定の機能を有効/無効にできます。このコミット以前は、precisestackもこのGOEXPERIMENTを通じて制御されていたと考えられます。

技術的詳細

precisestackの導入は、Goのガベージコレクタのスタックスキャン戦略における重要な改善です。

  1. スタックのゼロ初期化: precisestackを有効にすると、関数が呼び出された際に、その関数のスタックフレーム内でポインタを格納する可能性のあるすべての場所が、エントリ時にゼロ(nilポインタ)で初期化されるようになります。これにより、GCがスタックをスキャンする際に、未初期化のメモリをポインタとして誤認識するリスクがなくなります。GCは、ゼロ値の場所はポインタではないか、あるいはnilポインタであると安全に判断できます。

  2. インターフェースの安全性向上: インターフェース値は、その性質上、型情報とデータポインタの両方を持つため、GCの正確なスキャンが特に重要です。precisestackにより、インターフェース変数が使用される前にその内部表現が確実に初期化されるため、GCがインターフェースをスキャンする際の信頼性が大幅に向上します。これにより、インターフェース関連のGCバグやクラッシュのリスクが低減されます。

  3. 性能コストと最適化: スタックのゼロ初期化は、追加のCPUサイクルを必要とするため、一時的に性能オーバーヘッドが発生します。コミットメッセージによると、この時点ではall.bash(Goのテストスイート全体を実行するスクリプト)で約5%の実行時間増加が見られました。しかし、このコストはGo 1.3のリリースまでに最適化される予定であり、issue 7345で追跡されていました。これは、Go開発チームが正確性向上のために一時的な性能低下を許容しつつ、最終的な性能目標を達成する意図があったことを示しています。

  4. 既知のGCバグとの関連: コミットメッセージでは、issue 7343と7344という2つの既知のGCバグに言及しています。

    • issue 7343はprecisestackがなくても発生するバグであり、precisestackとは独立した問題です。
    • issue 7344はprecisestackを有効にした場合にのみ観測されたバグですが、コミットメッセージはprecisestackが直接の原因ではない可能性を示唆しています。むしろ、precisestackが既存の潜在的な問題を顕在化させている、あるいは悪化させている可能性が指摘されています。これは、GCの正確性が向上したことで、これまで隠れていたメモリ管理の不整合が露呈したことを意味するかもしれません。これらのバグもGo 1.3で修正が予定されていました。
  5. 将来のGC戦略への布石: 「copying stack work depends on it」という記述は、GoのGCが将来的にスタックをコピーする方式(例えば、スタックをヒープに移動させたり、スタックのサイズ変更を効率化したりする目的)を採用する可能性を示唆しています。スタックをコピーするGCでは、スタック上のポインタを完全に正確に識別できることが絶対条件となります。precisestackは、この将来のGCアーキテクチャの基盤を築くものです。

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

このコミットでは、主に2つのファイルが変更されています。

  1. src/cmd/gc/lex.c: Goコンパイラのフロントエンドの一部で、実験的機能の有効/無効を制御する部分です。

    --- a/src/cmd/gc/lex.c
    +++ b/src/cmd/gc/lex.c
    @@ -60,7 +60,7 @@ static void
     addexp(char *s)
     {
     	int i;
    -	
    +
     	for(i=0; exper[i].name != nil; i++) {
     		if(strcmp(exper[i].name, s) == 0) {
     			*exper[i].val = 1;
    @@ -77,7 +77,9 @@ setexp(void)
     {
     	char *f[20];
     	int i, nf;
    -	
    +
    +	precisestack_enabled = 1; // on by default
    +
     	// The makefile #defines GOEXPERIMENT for us.
     	nf = getfields(GOEXPERIMENT, f, nelem(f), 1, ",");
     	for(i=0; i<nf; i++)
    
  2. src/pkg/runtime/proc.c: Goランタイムの初期化処理の一部で、precisestack機能の有効/無効をランタイムレベルで制御する部分です。

    --- a/src/pkg/runtime/proc.c
    +++ b/src/pkg/runtime/proc.c
    @@ -144,7 +144,7 @@ runtime·schedinit(void)
     	Eface i;
     
     	runtime·sched.maxmcount = 10000;
    -	runtime·precisestack = haveexperiment("precisestack");
    +	runtime·precisestack = true; // haveexperiment("precisestack");
     
     	runtime·mallocinit();
     	mcommoninit(m);
    

コアとなるコードの解説

src/cmd/gc/lex.c の変更

setexp関数は、Goコンパイラが起動する際に、GOEXPERIMENT環境変数で指定された実験的機能を有効にするための処理を行います。 変更前は、precisestack機能はGOEXPERIMENT環境変数を通じてのみ有効にすることができました。 変更後、precisestack_enabled = 1; // on by defaultという行が追加されました。これは、GOEXPERIMENTの設定に関わらず、コンパイラレベルでprecisestack機能をデフォルトで有効にするという明確な指示です。これにより、コンパイルされたGoプログラムは、precisestackの恩恵を受けるようになります。

src/pkg/runtime/proc.c の変更

runtime·schedinit関数は、Goランタイムの初期化処理を行う重要な関数です。 変更前は、runtime·precisestack = haveexperiment("precisestack");という行があり、ランタイムがprecisestack機能を有効にするかどうかをGOEXPERIMENT環境変数の設定に依存していました。haveexperiment関数は、指定された実験的機能が有効になっているかをチェックします。 変更後、この行はruntime·precisestack = true; // haveexperiment("precisestack");に変更されました。これは、ランタイムレベルでもprecisestack機能を無条件にtrue(有効)に設定することを意味します。コメントアウトされたhaveexperiment("precisestack")は、以前の挙動を示唆しつつ、もはやそのチェックが不要になったことを示しています。

これらの変更は、コンパイラとランタイムの両方でprecisestack機能をデフォルトで有効にすることで、Goプログラム全体でスタックの正確なスキャンが保証されるように連携しています。

関連リンク

参考にした情報源リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事 (当時のGo 1.3リリースノートやGCに関する設計ドキュメントなど)
  • Goのソースコード内のコメントや関連するコミット履歴
  • GoのIssueトラッカー (上記で参照したIssue 7343, 7344, 7345など)
  • Goのインターフェースの内部表現に関する技術記事
  • ガベージコレクションの一般的な概念に関するコンピュータサイエンスの文献
  • https://go.dev/doc/go1.3 (Go 1.3 Release Notes - precisestackに関する言及がある可能性)
  • https://go.dev/blog/go1.3gc (Go 1.3のGCに関するブログ記事があれば、詳細な情報源となる)
  • https://go.dev/src/runtime/ (Goランタイムのソースコード)
  • https://go.dev/src/cmd/gc/ (Goコンパイラのソースコード)
  • https://go.dev/issue/7345
  • https://go.dev/issue/7343
  • https://go.dev/issue/7344