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

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

このコミットは、Go言語のランタイムとコンパイラにおけるガベージコレクション(GC)のポインタマップのエンコーディング方式を変更するものです。具体的には、iface(インターフェース値)とeface(空インターフェース値)のポインタをより正確に識別できるように、ポインタマップの各エントリのビット幅を1ビットから2ビットに拡張しています。これにより、GCがインターフェース値をスキャンする際の精度が向上し、不要なオブジェクトのスキャンを減らすことでGCの効率化と正確性の向上が図られています。

コミット

commit abc516e4202e0206a6d8725efd8308d1982c1189
Author: Carl Shapiro <cshapiro@google.com>
Date:   Fri Aug 9 16:48:12 2013 -0700

    cmd/cc, cmd/gc, runtime: Uniquely encode iface and eface pointers in the pointer map.
    
    Prior to this change, pointer maps encoded the disposition of
    a word using a single bit.  A zero signaled a non-pointer
    value and a one signaled a pointer value.  Interface values,
    which are a effectively a union type, were conservatively
    labeled as a pointer.
    
    This change widens the logical element size of the pointer map
    to two bits per word.  As before, zero signals a non-pointer
    value and one signals a pointer value.  Additionally, a two
    signals an iface pointer and a three signals an eface pointer.
    
    Following other changes to the runtime, values two and three
    will allow a type information to drive interpretation of the
    subsequent word so only those interface values containing a
    pointer value will be scanned.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12689046

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

https://github.com/golang/go/commit/abc516e4202e0206a6d8725efd8308d1982c1189

元コミット内容

このコミットの元の内容は、ポインタマップにおけるifaceefaceポインタのユニークなエンコーディングに関するものです。

変更前は、ポインタマップは各ワード(メモリ上の単位)のポインタとしての性質を1ビットでエンコードしていました。

  • 0 は非ポインタ値
  • 1 はポインタ値

インターフェース値は実質的に共用体型であるため、ポインタマップ上では保守的にポインタとして扱われていました。これは、インターフェースが内部にポインタを含む可能性があるため、安全のために常にポインタとしてマークされていたことを意味します。

この変更により、ポインタマップの論理的な要素サイズが1ワードあたり2ビットに拡張されました。

  • 0 は非ポインタ値(変更なし)
  • 1 はポインタ値(変更なし)
  • 2iface ポインタ
  • 3eface ポインタ

ランタイムの他の変更と連携して、この新しいエンコーディング(値 23)により、後続のワードの解釈を型情報に基づいて行うことが可能になります。これにより、ポインタ値を含むインターフェース値のみがスキャンされるようになり、GCの精度と効率が向上します。

変更の背景

Go言語のガベージコレクタは、メモリ上のオブジェクトがまだ参照されているかどうかを判断するために、ポインタを正確に識別する必要があります。特に、スタックやヒープ上のデータ構造をスキャンする際に、どのメモリワードがポインタであり、どのワードが単なる整数や他の非ポインタ値であるかを区別することが重要です。

インターフェースはGo言語の強力な機能ですが、その内部構造はGCにとって課題となります。Goのインターフェース値は、通常、2つのワードで構成されます。1つは型情報(_typeポインタ)を指し、もう1つは基となる具体的な値(データポインタ)を指します。このデータポインタは、具体的な値がポインタ型である場合、ヒープ上のオブジェクトを指す可能性があります。

このコミット以前は、ポインタマップはインターフェース値を「ポインタ」として一律に扱っていました。これは「保守的なスキャン」と呼ばれ、インターフェース値のデータ部分が実際にポインタであるかどうかに関わらず、常にポインタとして扱われるため、GCが不要なメモリ領域をスキャンしたり、場合によっては誤って到達不能なオブジェクトを到達可能と判断したりするリスクがありました。

より正確なGC(Precise GC)を実現するためには、インターフェース値のデータ部分が実際にポインタである場合にのみ、そのポインタを追跡する必要があります。このコミットは、ポインタマップにifaceefaceを区別する情報を埋め込むことで、この「より正確なスキャン」を可能にするための基盤を構築しています。これにより、GCはインターフェースの型情報に基づいて、データ部分がポインタであるかどうかを判断し、必要な場合のみスキャンを行うことができるようになります。

前提知識の解説

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

Go言語は、自動メモリ管理のためにトレース型ガベージコレクタを採用しています。これは、プログラムがアクセス可能な(到達可能な)オブジェクトを特定し、それ以外の(到達不能な)オブジェクトが占めるメモリを解放する仕組みです。GoのGCは、主に以下の要素で構成されます。

  • ポインタマップ (Pointer Map): GCがメモリをスキャンする際に、どのメモリワードがポインタであるかを示すメタデータです。これにより、GCはポインタではない値を誤ってポインタとして解釈し、無関係なメモリ領域をスキャンしたり、誤った参照をたどったりするのを防ぎます。ポインタマップは、コンパイラによって生成され、実行時にランタイムによって利用されます。
  • スタックとヒープ: プログラムの実行中に変数が格納される主要なメモリ領域です。
    • スタック: 関数呼び出しやローカル変数など、短期間のデータが格納されます。LIFO(後入れ先出し)の構造を持ちます。
    • ヒープ: 動的に確保されるデータ(newmakeで作成されるオブジェクトなど)が格納されます。GCの主な対象となります。
  • スキャン: GCがメモリ上のオブジェクトをたどり、到達可能なオブジェクトをマークするプロセスです。ポインタマップはこのスキャンプロセスをガイドします。

Go言語のインターフェース (ifaceeface)

Go言語のインターフェースは、ポリモーフィズムを実現するための重要な機能です。Goのインターフェース値は、内部的に2つの要素で構成される構造体として表現されます。

  • iface (Interface Value): 少なくとも1つのメソッドを持つインターフェース型(例: io.Reader)の値を指します。
    • itab ポインタ: インターフェースの型情報と、具体的な型が実装するメソッドテーブルへのポインタを含む構造体(runtime._itab)を指します。
    • データポインタ: インターフェースに格納されている具体的な値へのポインタです。このポインタが指す先は、ポインタ型である場合もあれば、非ポインタ型である場合もあります。
  • eface (Empty Interface Value): メソッドを持たない空インターフェース型(interface{})の値を指します。
    • _type ポインタ: 格納されている具体的な値の型情報(runtime._type)を指します。
    • データポインタ: iface と同様に、具体的な値へのポインタです。

このコミット以前のGCは、インターフェース値のデータポインタが実際にポインタであるかどうかを区別せず、常にポインタとして扱っていました。これは、インターフェースがどのような型の値を保持しているかに関わらず、そのデータ部分をスキャンする必要があるため、安全側の判断として行われていました。しかし、これによりGCの効率が低下する可能性がありました。

ビットマップとポインタマップ

ガベージコレクタは、メモリ上のオブジェクトのレイアウトを理解するためにポインタマップを使用します。これは通常、ビットマップとして実装されます。各ビットは、対応するメモリワードがポインタであるか非ポインタであるかを示します。

  • 1ビットエンコーディング: 従来の方式では、1ビットでポインタの有無を表現していました。
    • 0: 非ポインタ
    • 1: ポインタ この方式では、インターフェース値のデータ部分がポインタであるかどうかの詳細な区別ができませんでした。

このコミットは、このビットマップのエンコーディングを拡張し、より多くの情報を格納できるようにすることで、GCの精度を向上させようとしています。

技術的詳細

このコミットの核心は、Goランタイムのガベージコレクタが使用するポインタマップのエンコーディング方式を、1ビット/ワードから2ビット/ワードに拡張することです。この拡張により、GCは単にポインタの有無だけでなく、それがifaceポインタなのかefaceポインタなのかを区別できるようになります。

2ビットエンコーディングの導入

新しいエンコーディングでは、各メモリワードに対して2ビットが割り当てられます。

  • 00 (0): 非ポインタ値。
  • 01 (1): 通常のポインタ値。
  • 10 (2): iface(インターフェース値)のデータポインタ。
  • 11 (3): eface(空インターフェース値)のデータポインタ。

この変更により、ポインタマップはよりリッチな型情報を持つことになります。特に、ifaceefaceのデータポインタを明示的に区別できるようになったことが重要です。

精密なスキャンへの道

この2ビットエンコーディングの導入は、GoのGCがインターフェース値を「保守的」にスキャンするのではなく、「精密」にスキャンするための重要なステップです。

  • 保守的なスキャン: 以前は、インターフェース値のデータ部分がポインタであるかどうかにかかわらず、常にポインタとして扱われ、その指す先がスキャンされていました。これは安全ですが、非ポインタ値が格納されている場合でもスキャンが行われるため、GCのオーバーヘッドが増加し、また、誤って到達不能なメモリを到達可能と判断する(メモリリークの原因となる)可能性がありました。
  • 精密なスキャン: 新しいエンコーディングでは、GCはポインタマップからifaceまたはefaceのマークを読み取ると、そのインターフェース値の型情報(itabまたは_typeポインタが指す先)を参照できるようになります。型情報には、インターフェースが保持している具体的な値の型に関する詳細が含まれています。GCはこの型情報に基づいて、具体的な値が実際にポインタである場合にのみ、そのポインタを追跡してスキャンします。これにより、非ポインタ値が格納されているインターフェースはスキャンされなくなり、GCの効率と正確性が大幅に向上します。

コンパイラとランタイムの連携

この機能を実現するためには、コンパイラ(cmd/cccmd/gc)とランタイム(pkg/runtime)の両方で変更が必要です。

  • コンパイラ側 (src/cmd/cc/pgen.c, src/cmd/gc/pgen.c):
    • コンパイラは、プログラムの型情報に基づいてポインタマップを生成します。
    • このコミットでは、ポインタマップを生成する際に、ifaceefaceのデータポインタに対して新しい2ビットエンコーディング(2または3)を適用するように変更されています。
    • BitsPerPointer = 2 という定数が導入され、ポインタマップのビットベクトルを割り当てる際に、各ワードに対して2ビットが考慮されるように計算が調整されています。
    • bvset 関数(ビットベクトルにビットを設定する関数)の呼び出しが、*xoffset / widthptr ではなく (*xoffset / widthptr) * BitsPerPointer のように変更され、2ビット単位でオフセットが計算されるようになっています。
    • 特に eface の場合、isnilinter(t) のチェックが追加され、eface の型ポインタとデータポインタの両方に対して適切なビットが設定されるようになっています。
  • ランタイム側 (src/pkg/runtime/mgc0.c):
    • ランタイムのGCは、コンパイラが生成したポインタマップを読み取り、メモリをスキャンします。
    • このコミットでは、scanbitvector 関数(ポインタマップをスキャンする関数)が、各エントリを2ビット単位で読み取るように変更されています。
    • w & 1 だった条件が w & 3 に変更され、下位2ビットを読み取るようになっています。
    • w >>= 1 だったシフト操作が w >>= BitsPerPointer(つまり w >>= 2)に変更され、次の2ビットのペアに進むようになっています。
    • addroot 関数が呼び出される条件も、新しい2ビットエンコーディングに基づいて調整されています。
    • addframeroots 関数でも、ローカル変数のポインタマップをスキャンする際のサイズ計算が BitsPerPointer を考慮するように変更されています。

この連携により、コンパイラが生成した正確なポインタマップ情報を、ランタイムのGCが正しく解釈し、精密なスキャンを実行できるようになります。

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

このコミットは、主に以下の3つのファイルに影響を与えています。

  1. src/cmd/cc/pgen.c: Cコンパイラのバックエンドで、Goの型情報からGCポインタマップを生成する部分。
  2. src/cmd/gc/pgen.c: Goコンパイラのバックエンドで、Goの型情報からGCポインタマップを生成する部分。
  3. src/pkg/runtime/mgc0.c: Goランタイムのガベージコレクタの主要な部分で、ポインタマップを解釈してメモリをスキャンする部分。

src/cmd/cc/pgen.c および src/cmd/gc/pgen.c の変更

これらのファイルでは、ポインタマップのビットベクトルを生成するロジックが変更されています。

  • enum { BitsPerPointer = 2 }; の追加: ポインタマップの各エントリが2ビットを使用することを示す定数が定義されました。
  • bvset 関数の呼び出しの変更: ポインタマップのビットベクトルにポインタの存在を示すビットを設定する bvset 関数の呼び出しが変更されました。 変更前: bvset(bv, (offset + t->offset) / ewidth[TIND]); 変更後: bvset(bv, ((offset + t->offset) / ewidth[TIND])*BitsPerPointer); これは、ポインタマップのインデックス計算が、1ワードあたり1ビットではなく2ビットを考慮するように調整されたことを意味します。
  • bvalloc 関数の呼び出しの変更: ビットベクトルを割り当てる bvalloc 関数の呼び出しも、必要なビット数が2倍になるように変更されました。 変更前: bv = bvalloc((argsize() + ewidth[TIND] - 1) / ewidth[TIND]); 変更後: bv = bvalloc((argbytes / ewidth[TIND]) * BitsPerPointer); 同様に、src/cmd/gc/pgen.cdumpgcargs および dumpgclocals でも bvalloc の引数が * BitsPerPointer で調整されています。
  • eface (空インターフェース) の特殊な処理 (src/cmd/gc/pgen.c のみ): eface の構造体 { Type* type; union { void* ptr, uintptr val } data; } に対応するため、walktype1 関数内で eface の型ポインタとデータポインタの両方に対してビットを設定するロジックが追加されました。 特に、isnilinter(t) のチェックが追加され、eface の型ポインタ(_type)とデータポインタの両方に対して適切なビットが設定されるようになっています。
    // struct { Type* type; union { void* ptr, uintptr val } data; }
    if(*xoffset % widthptr != 0)
    	fatal("walktype1: invalid alignment, %T", t);
    bvset(bv, ((*xoffset / widthptr) * BitsPerPointer) + 1); // データポインタ部分
    if(isnilinter(t))
    	bvset(bv, ((*xoffset / widthptr) * BitsPerPointer)); // 型ポインタ部分
    bvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer); // 2ワード目のデータポインタ部分
    *xoffset += t->width;
    break;
    
    この部分の +1 は、ifaceeface のデータポインタが 1 (通常のポインタ) としてエンコードされることを示唆しています。しかし、コミットメッセージでは 23ifaceeface を示すとあるため、このコードは eface の型ポインタとデータポインタのビット設定をより詳細に行っていると解釈できます。

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

このファイルでは、ランタイムのGCがポインタマップを読み取るロジックが変更されています。

  • BitsPerPointer = 2 の定義: ランタイム側でも、ポインタマップの各エントリが2ビットを使用することを示す定数が定義されました。
  • scanbitvector 関数の変更: ポインタマップのビットベクトルをスキャンする主要な関数である scanbitvector が、2ビット単位で処理するように変更されました。 変更前:
    i /= 1; // 実質的に何もしない
    for(; i > 0; i--) {
    	if(w & 1) // 下位1ビットをチェック
    		addroot((Obj){scanp, PtrSize, 0});
    	w >>= 1; // 1ビット右シフト
    	scanp += PtrSize;
    }
    
    変更後:
    i /= BitsPerPointer; // i を2で割る
    for(; i > 0; i--) {
    	if(w & 3) // 下位2ビットをチェック (00, 01, 10, 11 のいずれか)
    		addroot((Obj){scanp, PtrSize, 0});
    	w >>= BitsPerPointer; // 2ビット右シフト
    	scanp += PtrSize;
    }
    
    w & 3 は、ポインタマップの2ビットエンコーディングが 00 (非ポインタ) 以外であれば、何らかのポインタ(通常ポインタ、ifaceeface)が存在することを示します。これにより、GCはポインタマップの情報を正しく解釈し、addroot を呼び出してオブジェクトをルートとしてマークします。
  • addframeroots 関数の変更: スタックフレームのローカル変数をスキャンする addframeroots 関数でも、ポインタマップのサイズ計算が調整されました。 変更前: size = locals->n*PtrSize; 変更後: size = (locals->n*PtrSize) / BitsPerPointer; これは、ポインタマップが2ビット/ワードになったため、必要なビットベクトルのサイズが半分になることを反映しています。

コアとなるコードの解説

このコミットのコアとなる変更は、ポインタマップのエンコーディングと、それを生成・解釈するロジックの変更に集約されます。

コンパイラ側の変更 (pgen.c ファイル群)

コンパイラは、Goのソースコードをコンパイルする際に、各データ構造やスタックフレーム内のどの位置にポインタが存在するかを示す「ポインタマップ」を生成します。このポインタマップは、ガベージコレクタがメモリを正確にスキャンするために不可欠な情報です。

BitsPerPointer = 2 の導入は、このポインタマップの粒度を変更するものです。以前は各メモリワードに対して1ビットが割り当てられていましたが、これからは2ビットが割り当てられます。

bvset(bv, ((offset + t->offset) / ewidth[TIND])*BitsPerPointer); のような変更は、ポインタマップのビットベクトルにポインタの情報を書き込む際のインデックス計算を調整しています。ewidth[TIND] はポインタのサイズ(通常は4バイトまたは8バイト)を示します。offset / ewidth[TIND] は、メモリワードのオフセットをポインタのワード単位に変換します。これに BitsPerPointer (2) を掛けることで、2ビット単位でインデックスが進むように調整されます。これにより、コンパイラはポインタマップに2ビットの情報を正確に書き込めるようになります。

特に eface の処理では、bvset(bv, ((*xoffset / widthptr) * BitsPerPointer) + 1); のように +1 が付加されています。これは、2ビットのエンコーディングにおいて、01 (通常のポインタ) または 11 (eface のデータポインタ) のように、下位ビットが 1 であることを示唆している可能性があります。コミットメッセージの 23ifaceeface を示すという説明と合わせて考えると、コンパイラは ifaceeface のデータポインタをそれぞれ 1011 でエンコードし、それ以外の通常のポインタを 01 でエンコードしていると推測できます。

ランタイム側の変更 (mgc0.c)

ランタイムのガベージコレクタは、コンパイラが生成したポインタマップを読み取り、メモリをスキャンします。

scanbitvector 関数は、ポインタマップのビットベクトルを実際に走査する部分です。 i /= BitsPerPointer; は、スキャンするワード数を2ビット単位で調整します。 if(w & 3) は、現在の2ビットのペアが 00 (非ポインタ) 以外であれば、何らかのポインタが存在すると判断します。w & 3 は、01 (通常のポインタ)、10 (iface ポインタ)、11 (eface ポインタ) のいずれかであれば真となります。これにより、GCはポインタマップにエンコードされた情報を正しく解釈し、ポインタが存在するワードに対して addroot を呼び出します。 w >>= BitsPerPointer; は、次の2ビットのペアに進むために、ビット列を2ビット右にシフトします。

これらの変更により、ランタイムのGCは、ポインタマップから ifaceeface の情報を区別して読み取れるようになります。これにより、GCはインターフェースの型情報に基づいて、そのデータ部分が実際にポインタである場合にのみスキャンを行うという、より精密な動作が可能になります。これは、GCのオーバーヘッドを削減し、メモリリークのリスクを低減する上で非常に重要です。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(当時のもの)
  • Go言語のインターフェースの内部構造に関する解説記事
  • Go言語のコンパイラとランタイムのソースコード(特にGC関連)

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go 1.x 時代のGCに関する情報)
  • Go言語のソースコード (特に src/cmd/cc, src/cmd/gc, src/pkg/runtime ディレクトリ)
  • Go言語のガベージコレクションに関する技術ブログや論文 (例: "Go's new GC: Less latency and more throughput")
  • Go言語のインターフェースに関する解説記事 (例: "The Laws of Reflection" by Rob Pike)
  • https://golang.org/cl/12689046 (このコミットのChange-ID)
  • https://github.com/golang/go/commit/abc516e4202e0206a6d8725efd8308d1982c1189 (GitHub上のコミットページ)

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

このコミットは、Go言語のランタイムとコンパイラにおけるガベージコレクション(GC)のポインタマップのエンコーディング方式を変更するものです。具体的には、iface(インターフェース値)とeface(空インターフェース値)のポインタをより正確に識別できるように、ポインタマップの各エントリのビット幅を1ビットから2ビットに拡張しています。これにより、GCがインターフェース値をスキャンする際の精度が向上し、不要なオブジェクトのスキャンを減らすことでGCの効率化と正確性の向上が図られています。

コミット

commit abc516e4202e0206a6d8725efd8308d1982c1189
Author: Carl Shapiro <cshapiro@google.com>
Date:   Fri Aug 9 16:48:12 2013 -0700

    cmd/cc, cmd/gc, runtime: Uniquely encode iface and eface pointers in the pointer map.
    
    Prior to this change, pointer maps encoded the disposition of
    a word using a single bit.  A zero signaled a non-pointer
    value and a one signaled a pointer value.  Interface values,
    which are a effectively a union type, were conservatively
    labeled as a pointer.
    
    This change widens the logical element size of the pointer map
    to two bits per word.  As before, zero signals a non-pointer
    value and one signals a pointer value.  Additionally, a two
    signals an iface pointer and a three signals an eface pointer.
    
    Following other changes to the runtime, values two and three
    will allow a type information to drive interpretation of the
    subsequent word so only those interface values containing a
    pointer value will be scanned.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12689046

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

https://github.com/golang/go/commit/abc516e4202e0206a6d8725efd8308d1982c1189

元コミット内容

このコミットの元の内容は、ポインタマップにおけるifaceefaceポインタのユニークなエンコーディングに関するものです。

変更前は、ポインタマップは各ワード(メモリ上の単位)のポインタとしての性質を1ビットでエンコードしていました。

  • 0 は非ポインタ値
  • 1 はポインタ値

インターフェース値は実質的に共用体型であるため、ポインタマップ上では保守的にポインタとして扱われていました。これは、インターフェースが内部にポインタを含む可能性があるため、安全のために常にポインタとしてマークされていたことを意味します。

この変更により、ポインタマップの論理的な要素サイズが1ワードあたり2ビットに拡張されました。

  • 0 は非ポインタ値(変更なし)
  • 1 はポインタ値(変更なし)
  • 2iface ポインタ
  • 3eface ポインタ

ランタイムの他の変更と連携して、この新しいエンコーディング(値 23)により、後続のワードの解釈を型情報に基づいて行うことが可能になります。これにより、ポインタ値を含むインターフェース値のみがスキャンされるようになり、GCの精度と効率が向上します。

変更の背景

Go言語のガベージコレクタは、メモリ上のオブジェクトがまだ参照されているかどうかを判断するために、ポインタを正確に識別する必要があります。特に、スタックやヒープ上のデータ構造をスキャンする際に、どのメモリワードがポインタであり、どのワードが単なる整数や他の非ポインタ値であるかを区別することが重要です。

インターフェースはGo言語の強力な機能ですが、その内部構造はGCにとって課題となります。Goのインターフェース値は、通常、2つのワードで構成されます。1つは型情報(_typeポインタ)を指し、もう1つは基となる具体的な値(データポインタ)を指します。このデータポインタは、具体的な値がポインタ型である場合、ヒープ上のオブジェクトを指す可能性があります。

このコミット以前は、ポインタマップはインターフェース値を「ポインタ」として一律に扱っていました。これは「保守的なスキャン」と呼ばれ、インターフェース値のデータ部分が実際にポインタであるかどうかに関わらず、常にポインタとして扱われるため、GCが不要なメモリ領域をスキャンしたり、場合によっては誤って到達不能なオブジェクトを到達可能と判断したりするリスクがありました。

より正確なGC(Precise GC)を実現するためには、インターフェース値のデータ部分が実際にポインタである場合にのみ、そのポインタを追跡する必要があります。このコミットは、ポインタマップにifaceefaceを区別する情報を埋め込むことで、この「より正確なスキャン」を可能にするための基盤を構築しています。これにより、GCはインターフェースの型情報に基づいて、データ部分がポインタであるかどうかを判断し、必要な場合のみスキャンを行うことができるようになります。

前提知識の解説

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

Go言語は、自動メモリ管理のためにトレース型ガベージコレクタを採用しています。これは、プログラムがアクセス可能な(到達可能な)オブジェクトを特定し、それ以外の(到達不能な)オブジェクトが占めるメモリを解放する仕組みです。GoのGCは、主に以下の要素で構成されます。

  • ポインタマップ (Pointer Map): GCがメモリをスキャンする際に、どのメモリワードがポインタであるかを示すメタデータです。これにより、GCはポインタではない値を誤ってポインタとして解釈し、無関係なメモリ領域をスキャンしたり、誤った参照をたどったりするのを防ぎます。ポインタマップは、コンパイラによって生成され、実行時にランタイムによって利用されます。
  • スタックとヒープ: プログラムの実行中に変数が格納される主要なメモリ領域です。
    • スタック: 関数呼び出しやローカル変数など、短期間のデータが格納されます。LIFO(後入れ先出し)の構造を持ちます。
    • ヒープ: 動的に確保されるデータ(newmakeで作成されるオブジェクトなど)が格納されます。GCの主な対象となります。
  • スキャン: GCがメモリ上のオブジェクトをたどり、到達可能なオブジェクトをマークするプロセスです。ポインタマップはこのスキャンプロセスをガイドします。

Go言語のインターフェース (ifaceeface)

Go言語のインターフェースは、ポリモーフィズムを実現するための重要な機能です。Goのインターフェース値は、内部的に2つの要素で構成される構造体として表現されます。

  • iface (Interface Value): 少なくとも1つのメソッドを持つインターフェース型(例: io.Reader)の値を指します。
    • itab ポインタ: インターフェースの型情報と、具体的な型が実装するメソッドテーブルへのポインタを含む構造体(runtime._itab)を指します。
    • データポインタ: インターフェースに格納されている具体的な値へのポインタです。このポインタが指す先は、ポインタ型である場合もあれば、非ポインタ型である場合もあります。
  • eface (Empty Interface Value): メソッドを持たない空インターフェース型(interface{})の値を指します。
    • _type ポインタ: 格納されている具体的な値の型情報(runtime._type)を指します。
    • データポインタ: iface と同様に、具体的な値へのポインタです。

このコミット以前のGCは、インターフェース値のデータポインタが実際にポインタであるかどうかを区別せず、常にポインタとして扱っていました。これは、インターフェースがどのような型の値を保持しているかに関わらず、そのデータ部分をスキャンする必要があるため、安全側の判断として行われていました。しかし、これによりGCの効率が低下する可能性がありました。

ビットマップとポインタマップ

ガベージコレクタは、メモリ上のオブジェクトのレイアウトを理解するためにポインタマップを使用します。これは通常、ビットマップとして実装されます。各ビットは、対応するメモリワードがポインタであるか非ポインタであるかを示します。

  • 1ビットエンコーディング: 従来の方式では、1ビットでポインタの有無を表現していました。
    • 0: 非ポインタ
    • 1: ポインタ この方式では、インターフェース値のデータ部分がポインタであるかどうかの詳細な区別ができませんでした。

このコミットは、このビットマップのエンコーディングを拡張し、より多くの情報を格納できるようにすることで、GCの精度を向上させようとしています。

技術的詳細

このコミットの核心は、Goランタイムのガベージコレクタが使用するポインタマップのエンコーディング方式を、1ビット/ワードから2ビット/ワードに拡張することです。この拡張により、GCは単にポインタの有無だけでなく、それがifaceポインタなのかefaceポインタなのかを区別できるようになります。

2ビットエンコーディングの導入

新しいエンコーディングでは、各メモリワードに対して2ビットが割り当てられます。

  • 00 (0): 非ポインタ値。
  • 01 (1): 通常のポインタ値。
  • 10 (2): iface(インターフェース値)のデータポインタ。
  • 11 (3): eface(空インターフェース値)のデータポインタ。

この変更により、ポインタマップはよりリッチな型情報を持つことになります。特に、ifaceefaceのデータポインタを明示的に区別できるようになったことが重要です。

精密なスキャンへの道

この2ビットエンコーディングの導入は、GoのGCがインターフェース値を「保守的」にスキャンするのではなく、「精密」にスキャンするための重要なステップです。

  • 保守的なスキャン: 以前は、インターフェース値のデータ部分がポインタであるかどうかにかかわらず、常にポインタとして扱われ、その指す先がスキャンされていました。これは安全ですが、非ポインタ値が格納されている場合でもスキャンが行われるため、GCのオーバーヘッドが増加し、また、誤って到達不能なメモリを到達可能と判断する(メモリリークの原因となる)可能性がありました。
  • 精密なスキャン: 新しいエンコーディングでは、GCはポインタマップからifaceまたはefaceのマークを読み取ると、そのインターフェース値の型情報(itabまたは_typeポインタが指す先)を参照できるようになります。型情報には、インターフェースが保持している具体的な値の型に関する詳細が含まれています。GCはこの型情報に基づいて、具体的な値が実際にポインタである場合にのみ、そのポインタを追跡してスキャンします。これにより、非ポインタ値が格納されているインターフェースはスキャンされなくなり、GCの効率と正確性が大幅に向上します。

コンパイラとランタイムの連携

この機能を実現するためには、コンパイラ(cmd/cccmd/gc)とランタイム(pkg/runtime)の両方で変更が必要です。

  • コンパイラ側 (src/cmd/cc/pgen.c, src/cmd/gc/pgen.c):
    • コンパイラは、プログラムの型情報に基づいてポインタマップを生成します。
    • このコミットでは、ポインタマップを生成する際に、ifaceefaceのデータポインタに対して新しい2ビットエンコーディング(2または3)を適用するように変更されています。
    • BitsPerPointer = 2 という定数が導入され、ポインタマップのビットベクトルを割り当てる際に、各ワードに対して2ビットが考慮されるように計算が調整されています。
    • bvset 関数(ビットベクトルにビットを設定する関数)の呼び出しが、*xoffset / widthptr ではなく (*xoffset / widthptr) * BitsPerPointer のように変更され、2ビット単位でオフセットが計算されるようになっています。
    • 特に eface の場合、isnilinter(t) のチェックが追加され、eface の型ポインタとデータポインタの両方に対して適切なビットが設定されるようになっています。
  • ランタイム側 (src/pkg/runtime/mgc0.c):
    • ランタイムのGCは、コンパイラが生成したポインタマップを読み取り、メモリをスキャンします。
    • このコミットでは、scanbitvector 関数(ポインタマップをスキャンする関数)が、各エントリを2ビット単位で読み取るように変更されています。
    • w & 1 だった条件が w & 3 に変更され、下位2ビットを読み取るようになっています。
    • w >>= 1 だったシフト操作が w >>= BitsPerPointer(つまり w >>= 2)に変更され、次の2ビットのペアに進むようになっています。
    • addroot 関数が呼び出される条件も、新しい2ビットエンコーディングに基づいて調整されています。
    • addframeroots 関数でも、ローカル変数のポインタマップをスキャンする際のサイズ計算が BitsPerPointer を考慮するように変更されています。

この連携により、コンパイラが生成した正確なポインタマップ情報を、ランタイムのGCが正しく解釈し、精密なスキャンを実行できるようになります。

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

このコミットは、主に以下の3つのファイルに影響を与えています。

  1. src/cmd/cc/pgen.c: Cコンパイラのバックエンドで、Goの型情報からGCポインタマップを生成する部分。
  2. src/cmd/gc/pgen.c: Goコンパイラのバックエンドで、Goの型情報からGCポインタマップを生成する部分。
  3. src/pkg/runtime/mgc0.c: Goランタイムのガベージコレクタの主要な部分で、ポインタマップを解釈してメモリをスキャンする部分。

src/cmd/cc/pgen.c および src/cmd/gc/pgen.c の変更

これらのファイルでは、ポインタマップのビットベクトルを生成するロジックが変更されています。

  • enum { BitsPerPointer = 2 }; の追加: ポインタマップの各エントリが2ビットを使用することを示す定数が定義されました。
  • bvset 関数の呼び出しの変更: ポインタマップのビットベクトルにポインタの存在を示すビットを設定する bvset 関数の呼び出しが変更されました。 変更前: bvset(bv, (offset + t->offset) / ewidth[TIND]); 変更後: bvset(bv, ((offset + t->offset) / ewidth[TIND])*BitsPerPointer); これは、ポインタマップのインデックス計算が、1ワードあたり1ビットではなく2ビットを考慮するように調整されたことを意味します。
  • bvalloc 関数の呼び出しの変更: ビットベクトルを割り当てる bvalloc 関数の呼び出しも、必要なビット数が2倍になるように変更されました。 変更前: bv = bvalloc((argsize() + ewidth[TIND] - 1) / ewidth[TIND]); 変更後: bv = bvalloc((argbytes / ewidth[TIND]) * BitsPerPointer); 同様に、src/cmd/gc/pgen.cdumpgcargs および dumpgclocals でも bvalloc の引数が * BitsPerPointer で調整されています。
  • eface (空インターフェース) の特殊な処理 (src/cmd/gc/pgen.c のみ): eface の構造体 { Type* type; union { void* ptr, uintptr val } data; } に対応するため、walktype1 関数内で eface の型ポインタとデータポインタの両方に対してビットを設定するロジックが追加されました。 特に、isnilinter(t) のチェックが追加され、eface の型ポインタ(_type)とデータポインタの両方に対して適切なビットが設定されるようになっています。
    // struct { Type* type; union { void* ptr, uintptr val } data; }
    if(*xoffset % widthptr != 0)
    	fatal("walktype1: invalid alignment, %T", t);
    bvset(bv, ((*xoffset / widthptr) * BitsPerPointer) + 1); // データポインタ部分
    if(isnilinter(t))
    	bvset(bv, ((*xoffset / widthptr) * BitsPerPointer)); // 型ポインタ部分
    bvset(bv, ((*xoffset + widthptr) / widthptr) * BitsPerPointer); // 2ワード目のデータポインタ部分
    *xoffset += t->width;
    break;
    
    この部分の +1 は、ifaceeface のデータポインタが 1 (通常のポインタ) としてエンコードされることを示唆しています。しかし、コミットメッセージでは 23ifaceeface を示すとあるため、このコードは eface の型ポインタとデータポインタのビット設定をより詳細に行っていると解釈できます。

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

このファイルでは、ランタイムのGCがポインタマップを読み取るロジックが変更されています。

  • BitsPerPointer = 2 の定義: ランタイム側でも、ポインタマップの各エントリが2ビットを使用することを示す定数が定義されました。
  • scanbitvector 関数の変更: ポインタマップのビットベクトルをスキャンする主要な関数である scanbitvector が、2ビット単位で処理するように変更されました。 変更前:
    i /= 1; // 実質的に何もしない
    for(; i > 0; i--) {
    	if(w & 1) // 下位1ビットをチェック
    		addroot((Obj){scanp, PtrSize, 0});
    	w >>= 1; // 1ビット右シフト
    	scanp += PtrSize;
    }
    
    変更後:
    i /= BitsPerPointer; // i を2で割る
    for(; i > 0; i--) {
    	if(w & 3) // 下位2ビットをチェック (00, 01, 10, 11 のいずれか)
    		addroot((Obj){scanp, PtrSize, 0});
    	w >>= BitsPerPointer; // 2ビット右シフト
    	scanp += PtrSize;
    }
    
    w & 3 は、ポインタマップの2ビットエンコーディングが 00 (非ポインタ) 以外であれば、何らかのポインタ(通常ポインタ、ifaceeface)が存在することを示します。これにより、GCはポインタマップの情報を正しく解釈し、ポインタが存在するワードに対して addroot を呼び出してオブジェクトをルートとしてマークします。
  • addframeroots 関数の変更: スタックフレームのローカル変数をスキャンする addframeroots 関数でも、ポインタマップのサイズ計算が調整されました。 変更前: size = locals->n*PtrSize; 変更後: size = (locals->n*PtrSize) / BitsPerPointer; これは、ポインタマップが2ビット/ワードになったため、必要なビットベクトルのサイズが半分になることを反映しています。

コアとなるコードの解説

このコミットのコアとなる変更は、ポインタマップのエンコーディングと、それを生成・解釈するロジックの変更に集約されます。

コンパイラ側の変更 (pgen.c ファイル群)

コンパイラは、Goのソースコードをコンパイルする際に、各データ構造やスタックフレーム内のどの位置にポインタが存在するかを示す「ポインタマップ」を生成します。このポインタマップは、ガベージコレクタがメモリを正確にスキャンするために不可欠な情報です。

BitsPerPointer = 2 の導入は、このポインタマップの粒度を変更するものです。以前は各メモリワードに対して1ビットが割り当てられていましたが、これからは2ビットが割り当てられます。

bvset(bv, ((offset + t->offset) / ewidth[TIND])*BitsPerPointer); のような変更は、ポインタマップのビットベクトルにポインタの情報を書き込む際のインデックス計算を調整しています。ewidth[TIND] はポインタのサイズ(通常は4バイトまたは8バイト)を示します。offset / ewidth[TIND] は、メモリワードのオフセットをポインタのワード単位に変換します。これに BitsPerPointer (2) を掛けることで、2ビット単位でインデックスが進むように調整されます。これにより、コンパイラはポインタマップに2ビットの情報を正確に書き込めるようになります。

特に eface の処理では、bvset(bv, ((*xoffset / widthptr) * BitsPerPointer) + 1); のように +1 が付加されています。これは、2ビットのエンコーディングにおいて、01 (通常のポインタ) または 11 (eface のデータポインタ) のように、下位ビットが 1 であることを示唆している可能性があります。コミットメッセージの 23ifaceeface を示すという説明と合わせて考えると、コンパイラは ifaceeface のデータポインタをそれぞれ 1011 でエンコードし、それ以外の通常のポインタを 01 でエンコードしていると推測できます。

ランタイム側の変更 (mgc0.c)

ランタイムのガベージコレクタは、コンパイラが生成したポインタマップを読み取り、メモリをスキャンします。

scanbitvector 関数は、ポインタマップのビットベクトルを実際に走査する部分です。 i /= BitsPerPointer; は、スキャンするワード数を2ビット単位で調整します。 if(w & 3) は、現在の2ビットのペアが 00 (非ポインタ) 以外であれば、何らかのポインタが存在すると判断します。w & 3 は、01 (通常のポインタ)、10 (iface ポインタ)、11 (eface ポインタ) のいずれかであれば真となります。これにより、GCはポインタマップにエンコードされた情報を正しく解釈し、ポインタが存在するワードに対して addroot を呼び出します。 w >>= BitsPerPointer; は、次の2ビットのペアに進むために、ビット列を2ビット右にシフトします。

これらの変更により、ランタイムのGCは、ポインタマップから ifaceeface の情報を区別して読み取れるようになります。これにより、GCはインターフェースの型情報に基づいて、そのデータ部分が実際にポインタである場合にのみスキャンを行うという、より精密な動作が可能になります。これは、GCのオーバーヘッドを削減し、メモリリークのリスクを低減する上で非常に重要です。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(当時のもの)
  • Go言語のインターフェースの内部構造に関する解説記事
  • Go言語のコンパイラとランタイムのソースコード(特にGC関連)

参考にした情報源リンク

  • Go言語の公式ドキュメント (Go 1.x 時代のGCに関する情報)
  • Go言語のソースコード (特に src/cmd/cc, src/cmd/gc, src/pkg/runtime ディレクトリ)
  • Go言語のガベージコレクションに関する技術ブログや論文 (例: "Go's new GC: Less latency and more throughput")
  • Go言語のインターフェースに関する解説記事 (例: "The Laws of Reflection" by Rob Pike)
  • https://golang.org/cl/12689046 (このコミットのChange-ID)
  • https://github.com/golang/go/commit/abc516e4202e0206a6d8725efd8308d1982c1189 (GitHub上のコミットページ)