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

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

このコミットは、Goランタイムのガベージコレクション(GC)におけるスタックスキャン処理、特にインターフェース型の値の扱いに関する変更です。以前のコミット(CL 13010045 / 04f8101b46dd)を元に戻しつつ、その問題点を修正することを目的としています。具体的には、引数領域(arguments area)におけるインターフェース型のスキャン方法を調整し、32-bitビルドの破損を修正します。

コミット

commit 87fdb8fb9ab8de4e008fa7c1561b16e3df01223a
Author: Carl Shapiro <cshapiro@google.com>
Date:   Wed Aug 21 13:51:00 2013 -0700

    undo CL 13010045 / 04f8101b46dd
    
    Update the original change but do not read interface types in
    the arguments area.  Once the arguments area is zeroed as the
    locals area is we can safely read interface type values there
    too.
    
    ««« original CL description
    undo CL 12785045 / 71ce80dc4195
    
    This has broken the 32-bit builds.
    
    ««« original CL description
    cmd/gc, runtime: use type information to scan interface values
    
    R=golang-dev, rsc, dvyukov
    CC=golang-dev
    https://golang.org/cl/12785045
    »»»
    
    R=khr, golang-dev, khr
    CC=golang-dev
    https://golang.org/cl/13010045
    »»»
    
    R=khr, khr
    CC=golang-dev
    https://golang.org/cl/13073045
---
 src/cmd/gc/pgen.c      |  1 -
 src/pkg/runtime/mgc0.c | 54 ++++++++++++++++++++++++++++++++++++++++----------
 2 files changed, 43 insertions(+), 12 deletions(-)

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

https://github.com/golang/go/commit/87fdb8fb9ab8de4e008fa7c1561b16e3df01223a

元コミット内容

このコミットは、CL 13010045 / 04f8101b46dd を元に戻すものです。 CL 13010045 自体は、CL 12785045 / 71ce80dc4195 を元に戻すものでした。 CL 12785045 の内容は「cmd/gc, runtime: use type information to scan interface values」(型情報を使用してインターフェース値をスキャンする)というものでしたが、これが32-bitビルドを破損させたため、CL 13010045 で元に戻されました。

今回のコミット 87fdb8fb9ab8de4e008fa7c1561b16e3df01223a は、CL 13010045 を元に戻しつつ、元の変更(型情報を使ったインターフェース値のスキャン)を更新し、引数領域ではインターフェース型を読み取らないように修正しています。これは、引数領域がローカル変数領域と同様にゼロクリアされるようになれば、安全にインターフェース値を読み取れるようになるという前提に基づいています。

変更の背景

Goのガベージコレクション(GC)は、プログラムが使用しているメモリを追跡し、不要になったメモリを解放する役割を担っています。このプロセスの一部として、スタック上のポインタを正確に識別し、それらが指すオブジェクトをマークする必要があります。特に、インターフェース型の値は、内部にポインタを含む可能性があるため、GCが正しくスキャンすることが重要です。

以前のコミット CL 12785045 では、GCがインターフェース値をスキャンする際に型情報を利用するようになりました。これは、より正確で効率的なスキャンを可能にするための改善でしたが、結果として32-bitビルドで問題を引き起こしました。この問題は、おそらくスタック上の引数領域におけるインターフェース値の表現や、GCがその領域をスキャンする方法に起因するものと考えられます。

CL 13010045 は、この32-bitビルドの破損を修正するために、CL 12785045 の変更を一時的に元に戻しました。しかし、型情報を使ったスキャンは長期的に見て望ましい改善であるため、今回のコミット 87fdb8fb9ab8de4e008fa7c1561b16e3df01223a では、その変更を再導入しつつ、32-bitビルドの問題を回避するための修正が加えられました。具体的には、引数領域が安全にスキャンできるようになるまで、その領域のインターフェース型はスキャンしないというアプローチが取られています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムとガベージコレクションに関する知識が必要です。

  1. Goのガベージコレクション (GC): Goはトレース型GCを採用しており、到達可能性に基づいて不要なオブジェクトを特定し、メモリを解放します。GCは、ヒープだけでなく、スタック上のポインタもスキャンして、到達可能なオブジェクトをマークします。
  2. スタックスキャン: GCは実行中のGoroutineのスタックをスキャンし、スタック上に存在するポインタを識別します。これにより、スタックから参照されているヒープ上のオブジェクトがGCによって誤って解放されるのを防ぎます。
  3. ポインタマップ (Pointer Map): GoのGCは、スタックフレームやオブジェクト内のどこにポインタが存在するかを示す「ポインタマップ」を利用します。これにより、GCはメモリブロック全体をスキャンするのではなく、ポインタが確実に存在する場所だけを効率的にスキャンできます。このポインタマップは、コンパイラによって生成され、ランタイムに提供されます。
  4. インターフェース型 (Interface Types): Goのインターフェースは、内部的に2つのワードで構成されます。
    • 型情報 (Type Word): インターフェースが保持している具体的な値の型(_type構造体へのポインタ)。
    • 値 (Value Word): インターフェースが保持している具体的な値(ポインタまたは直接値)。 インターフェースがポインタ型の値を保持している場合、その値ワードはヒープ上のオブジェクトへのポインタとなります。GCは、このポインタを正しく識別し、参照先のオブジェクトをマークする必要があります。
  5. ItabEface:
    • Itab (Interface Table): 具象型がインターフェースを満たすための情報(型情報、メソッドテーブルなど)を格納する構造体です。interface{} ではない特定のインターフェース型(例: io.Reader)の場合に利用されます。
    • Eface (Empty Interface): interface{} 型を表す構造体です。型情報と値の2つのポインタで構成されます。 GCは、これらの構造体を解析して、内部に含まれるポインタを識別します。
  6. BitsPerPointer と関連する定数: src/pkg/runtime/mgc0.c に定義されているこれらの定数は、ポインタマップのビット表現に関連しています。
    • BitsPerPointer = 2: 各ポインタエントリが2ビットで表現されることを示します。
    • BitsNoPointer = 0: ポインタではないことを示します。
    • BitsPointer = 1: 通常のポインタであることを示します。
    • BitsIface = 2: 特定のインターフェース型(Itab を含む)であることを示します。
    • BitsEface = 3: 空のインターフェース型(Eface を含む)であることを示します。 これらのビットは、scanbitvector 関数で利用され、メモリ上の各ワードがポインタであるか、インターフェースであるか、あるいはポインタではないかをGCに伝えます。

技術的詳細

このコミットの主要な変更は、src/pkg/runtime/mgc0.c ファイル内のガベージコレクション関連のコードに集中しています。

  1. BitsPerPointer 関連の定数の追加: src/pkg/runtime/mgc0.cenum に、BitsNoPointer, BitsPointer, BitsIface, BitsEface の4つの定数が追加されました。これらは、ポインタマップの各2ビットが示す意味を明確にします。

    enum {
        // Pointer map
        BitsPerPointer = 2,
        BitsNoPointer = 0,
        BitsPointer = 1,
        BitsIface = 2,
        BitsEface = 3,
    };
    
  2. scaninterfacedata 関数の追加: この新しい関数は、インターフェース型がポインタであることを示す場合に、インターフェースのデータ部分をスキャンするために導入されました。 bits 引数によって BitsIface (特定のインターフェース) か BitsEface (空のインターフェース) かを判断し、それぞれ Itab または _type ポインタを介して型情報を取得します。 afterprologue 引数は、関数プロローグが完了しているかどうかを示し、スタックフレームが完全に設定されている場合にのみ型情報を安全に読み取れるようにします。 最終的に、インターフェースの値部分(scanp+PtrSize)をルートとしてGCに登録します。

    static void
    scaninterfacedata(uintptr bits, byte *scanp, bool afterprologue)
    {
        Itab *tab;
        Type *type;
    
        if(afterprologue) {
            if(bits == BitsIface) {
                tab = *(Itab**)scanp;
                if(tab->type->size <= sizeof(void*) && (tab->type->kind & KindNoPointers))
                    return;
            } else { // bits == BitsEface
                type = *(Type**)scanp;
                if(type->size <= sizeof(void*) && (type->kind & KindNoPointers))
                    return;
            }
        }
        addroot((Obj){scanp+PtrSize, PtrSize, 0});
    }
    
  3. scanbitvector 関数の変更: この関数は、ポインタマップ(BitVector)に基づいてメモリ領域をスキャンし、ポインタをGCルートとして登録します。 変更点として、afterprologue という新しい引数が追加されました。 ループ内で各2ビットの word を評価し、bitsBitsNoPointer でない場合、かつ値が nil でない場合に処理を行います。

    • BitsPointer の場合は、通常のポインタとして addroot します。
    • BitsIface または BitsEface の場合は、新しく追加された scaninterfacedata 関数を呼び出してインターフェースデータをスキャンします。
    static void
    scanbitvector(byte *scanp, BitVector *bv, bool afterprologue)
    {
        uintptr word, bits;
        uint32 *wordp;
        int32 i, remptrs;
    
        wordp = bv->data;
        for(remptrs = bv->n; remptrs > 0; remptrs -= 32) {
            word = *wordp++;
            if(remptrs < 32)
                i = remptrs;
            else
                i = 32;
            i /= BitsPerPointer;
            for(; i > 0; i--) {
                bits = word & 3;
                if(bits != BitsNoPointer && *(void**)scanp != nil)
                    if(bits == BitsPointer)
                        addroot((Obj){scanp, PtrSize, 0});
                    else
                        scaninterfacedata(bits, scanp, afterprologue);
                word >>= BitsPerPointer;
                scanp += PtrSize;
            }
        }
    }
    
  4. addframeroots 関数の変更: この関数は、スタックフレームのローカル変数と引数をGCルートとして登録します。

    • afterprologue という bool 型のローカル変数が追加され、frame->varp > (byte*)frame->sp の条件で初期化されます。これは、関数プロローグが完了し、ローカル変数領域が有効になっているかどうかを判断します。
    • ローカル変数(locals)のスキャンにおいて、scanbitvector の呼び出しに afterprologue 引数が渡されるようになりました。これにより、プロローグが完了していない場合はインターフェース型のスキャンが抑制されます。
    • 引数(args)のスキャンにおいて、scanbitvector の呼び出しに false が渡されるようになりました。これは、引数領域ではインターフェース型をスキャンしないというコミットの意図を反映しています。
    void
    addframeroots(Stkframe *frame, void*)
    {
        Func *f;
        BitVector *args, *locals;
        uintptr size;
        bool afterprologue; // 追加
    
        f = frame->fn;
    
        // Scan local variables if stack frame has been allocated.
        // Use pointer information if known.
        afterprologue = (frame->varp > (byte*)frame->sp); // 変更
        if(afterprologue) { // 変更
            locals = runtime·funcdata(f, FUNCDATA_GCLocals);
            if(locals == nil) {
                // No locals information, scan everything.
                addroot((Obj){frame->varp - frame->varlen, frame->varlen, 0});
            } else {
                // Locals bitmap information, scan just the
                // pointers in locals.
                size = (locals->n*PtrSize) / BitsPerPointer;
                scanbitvector(frame->varp - size, locals, afterprologue); // 変更
            }
        }
    
        // Scan arguments.
        // Use pointer information if known.
        args = runtime·funcdata(f, FUNCDATA_GCArgs);
        if(args != nil && args->n > 0)
            scanbitvector(frame->argp, args, false); // 変更
        else
            addroot((Obj){frame->argp, frame->arglen, 0});
    }
    
  5. src/cmd/gc/pgen.c の変更: walktype1 関数から以下の行が削除されました。

    bvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer);
    

    この行は、インターフェース型を処理する際に、ポインタマップに特定のビットを設定する役割を持っていました。この削除は、インターフェース型のスキャンロジックがランタイム側(mgc0.c)でより詳細に制御されるようになったことと関連している可能性があります。特に、引数領域でのインターフェース型のスキャンを抑制するという今回のコミットの目的と合致していると考えられます。

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

このコミットのコアとなる変更は、主に以下の2つのファイルにあります。

  1. src/pkg/runtime/mgc0.c:

    • BitsNoPointer, BitsPointer, BitsIface, BitsEface の定数定義の追加。
    • scaninterfacedata 関数の新規追加。
    • scanbitvector 関数のシグネチャ変更(bool afterprologue 引数の追加)と、インターフェース型スキャンロジックの追加(scaninterfacedata の呼び出し)。
    • addframeroots 関数の afterprologue 変数の導入と、scanbitvector 呼び出しへの afterprologue および false 引数の渡し方。
  2. src/cmd/gc/pgen.c:

    • walktype1 関数内の特定の bvset 呼び出しの削除。

コアとなるコードの解説

このコミットの核心は、Goのガベージコレクタがスタック上のインターフェース値をどのように扱うかを、より細かく制御することにあります。

以前の CL 12785045 では、コンパイラ(cmd/gc)とランタイム(runtime)が連携して、インターフェース値の型情報を利用してポインタをスキャンするようになりました。これは、インターフェースが保持する具体的な値がポインタであるかどうかを正確に判断し、不要なスキャンを避けるための最適化でした。しかし、この変更が32-bitビルドで問題を引き起こしました。

今回のコミットは、その問題を解決するために、以下の戦略を取っています。

  1. ポインタマップの粒度向上: BitsIfaceBitsEface という新しいビットパターンを導入することで、ポインタマップが単に「ポインタがあるかないか」だけでなく、「インターフェース型であるか」という情報も保持できるようになりました。これにより、ランタイムはインターフェース値をより具体的に識別できます。

  2. インターフェースデータスキャンの分離: scaninterfacedata 関数を導入することで、インターフェース値の内部スキャンロジックが独立しました。この関数は、インターフェースの型情報(Itab_type)を調べて、そのインターフェースが保持する値がポインタであるかどうか、そしてそのポインタがGCの対象となるべきかを判断します。

  3. プロローグ後の安全なスキャン: addframeroots 関数と scanbitvector 関数に afterprologue 引数を導入したことが重要です。関数が呼び出された直後(プロローグ中)は、スタックフレームが完全に構築されておらず、ローカル変数や引数領域がまだ初期化されていない可能性があります。この状態では、これらの領域に存在するインターフェース値の型情報を安全に読み取ることができません。

    • ローカル変数領域については、afterprologuetrue の場合(プロローグ完了後)にのみ、インターフェース型のスキャンが実行されます。
    • 引数領域については、scanbitvector に常に false が渡されるようになりました。これは、コミットメッセージにある「引数領域がローカル変数領域と同様にゼロクリアされるようになるまで、インターフェース型を読み取らない」という意図を直接反映しています。つまり、引数領域のインターフェース値は、その領域が安全にスキャンできる状態になるまで、型情報に基づいた詳細なスキャンは行わず、代わりに一般的なポインタとして扱われるか、あるいはスキャン自体が抑制されることを意味します。これにより、32-bitビルドで発生していた問題が回避されます。
  4. コンパイラ側の調整: src/cmd/gc/pgen.c からの bvset 呼び出しの削除は、コンパイラが生成するポインタマップのインターフェース型に関するビット設定を、ランタイム側の新しい、より洗練されたスキャンロジックに委ねることを示唆しています。これにより、コンパイラとランタイム間の責任分担が明確化され、ランタイムがインターフェース型のスキャンをより柔軟に制御できるようになります。

これらの変更により、GoのGCは、インターフェース値をより正確かつ安全にスキャンできるようになり、特にスタック上の引数領域における潜在的な問題を回避しつつ、型情報に基づいたスキャンという最適化の恩恵を享受できるようになりました。

関連リンク

参考にした情報源リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事 (当時のGoのバージョンに合わせたもの)
  • Goのインターフェースの内部表現に関する資料
  • Goのランタイムソースコード (src/pkg/runtime/mgc0.c および関連ファイル)
  • Goのコンパイラソースコード (src/cmd/gc/pgen.c および関連ファイル)