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

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

このコミットは、Goコンパイラ(gc)とランタイムにおいて、ガベージコレクション(GC)が型情報に基づいてより正確に動作するための変更を導入しています。具体的には、各Goの型について、そのメモリレイアウト内のどこにポインタが存在するかを示すGC情報を生成する機能が追加されました。これにより、GCはオブジェクトをスキャンする際に、ポインタではないデータ(整数、浮動小数点数など)を誤ってポインタとして解釈する「保守的なGC」から、より「正確なGC」へと進化し、メモリの回収効率と精度が向上します。

コミット

commit d09afc2efb50eeb3a1e55cbe05f42307ca0369b1
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Wed Sep 12 12:08:27 2012 -0400

    gc: generate garbage collection info for types
    
    R=rsc, nigeltao, minux.ma
    CC=golang-dev
    https://golang.org/cl/6290043

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

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

元コミット内容

gc: generate garbage collection info for types

R=rsc, nigeltao, minux.ma
CC=golang-dev
https://golang.org/cl/6290043

変更の背景

Go言語の初期のガベージコレクタは、スタックやヒープ上のオブジェクトをスキャンする際に、メモリ領域全体をポインタとして扱う「保守的なGC」の側面を持っていました。これは実装が容易である反面、実際にはポインタではないデータ(例えば、たまたまポインタのように見える整数値など)をポインタと誤認し、その結果、到達可能なオブジェクトを誤ってマークしてしまい、メモリを解放できないという問題(メモリリークではないが、メモリ使用効率の低下)を引き起こす可能性がありました。

このコミットの背景には、GCの精度と効率を向上させるという目的があります。型ごとに正確なポインタ情報をコンパイル時に生成することで、ランタイムのGCはオブジェクト内のどのオフセットにポインタが存在するかを正確に把握できるようになります。これにより、GCは不要になったオブジェクトをより確実に識別し、解放できるようになり、メモリ使用量の削減とGCの一時停止時間の短縮に貢献します。特に、複雑なデータ構造(ネストされた配列、構造体、マップ、インターフェースなど)を扱う際に、この正確な情報が不可欠となります。

前提知識の解説

  • ガベージコレクション (GC): プログラムが動的に確保したメモリ領域のうち、もはやどの部分からも参照されなくなった(到達不可能になった)領域を自動的に解放する仕組みです。Go言語はトレース型GCを採用しており、到達可能なオブジェクトをマークし、マークされなかったオブジェクトを解放します。
  • 正確なGC (Precise GC) と保守的なGC (Conservative GC):
    • 保守的なGC: メモリ領域内のすべてのワードをポインタである可能性があると仮定してスキャンします。これにより、誤ってポインタではない値をポインタと解釈し、到達可能なオブジェクトを誤ってマークしてしまう可能性があります。実装は比較的簡単ですが、メモリ使用効率が低下する可能性があります。
    • 正確なGC: オブジェクト内のどのオフセットにポインタが存在するかを正確に知っています。これにより、ポインタではない値を誤ってポインタと解釈することなく、到達可能なオブジェクトのみを正確にマークできます。メモリ使用効率が高く、GCの一時停止時間を短縮できますが、コンパイル時またはランタイム時に型ごとのポインタ情報を生成・管理する必要があります。
  • Goコンパイラ (gc): Go言語のソースコードを機械語に変換するツールです。このコミットでは、コンパイラが型情報からGCに必要なポインタ情報を生成する部分が変更されています。
  • Goランタイム (runtime): Goプログラムの実行を管理する部分です。GCはランタイムの一部として動作し、コンパイラが生成したGC情報を使用してメモリを管理します。
  • 型情報 (Type Information): Goプログラムの各変数や値がどのような型であるかを示すメタデータです。Goの型システムは静的型付けであり、コンパイル時に豊富な型情報が利用可能です。このコミットでは、この型情報がGCの精度向上のために活用されます。
  • ポインタマップ/GCプログラム: オブジェクトのメモリレイアウトにおいて、どのオフセットにポインタが存在するかを示すビットマップや、ポインタを走査するための命令列(プログラム)のことです。このコミットでは、後者の「命令列」に近い形式でGC情報が生成されます。

技術的詳細

このコミットの核心は、Goコンパイラが各型について、そのインスタンスがメモリ上でどのように配置され、どの位置にポインタが含まれているかを記述する「GC情報」を生成するメカニズムを導入した点にあります。このGC情報は、ランタイムのガベージコレクタがオブジェクトを走査する際に使用する一種の「バイトコード」のようなものです。

変更の主なポイントは以下の通りです。

  1. SymGcgen フラグの追加 (src/cmd/gc/go.h): Sym構造体に SymGcgen という新しいフラグが追加されました。これは、特定のシンボル(ここでは型に対応するGC情報シンボル)に対してGC情報が既に生成されたかどうかを追跡するために使用されます。これにより、同じ型に対してGC情報が重複して生成されるのを防ぎます。

  2. GC情報生成ロジックの導入 (src/cmd/gc/reflect.c):

    • dgcsym 関数が追加されました。この関数は、与えられた Type に対してGC情報を生成し、それをランタイムが参照できるシンボルとして出力する主要なエントリポイントです。
    • dgcsym1 関数が追加されました。これは dgcsym から呼び出される再帰的なヘルパー関数で、様々なGoの型(プリミティブ型、ポインタ、配列、スライス、構造体、マップ、文字列、インターフェースなど)のメモリレイアウトを走査し、対応するGC命令(オペコードと引数)を生成します。
    • dcommontype 関数が変更され、型の共通情報の一部として dgcsym(t) の結果(つまり、その型のGC情報シンボルへのポインタ)が埋め込まれるようになりました。これにより、ランタイムは型情報から直接その型のGC情報を取得できるようになります。
  3. GC命令の定義 (src/pkg/runtime/mgc0.h - 新規ファイル): mgc0.h は、GCがオブジェクトを走査するために使用する命令セット(オペコード)を定義する新しいヘッダーファイルです。これらの命令は、オブジェクト内のポインタの場所、配列の構造、スライスの要素、マップのキー/値、インターフェースのデータなどをGCに指示します。

    • GC_END: 命令列の終了。
    • GC_PTR: 型付きポインタ。オフセットと、そのポインタが指すオブジェクトのGC情報へのポインタを伴います。
    • GC_APTR: 任意のオブジェクトへのポインタ(型情報なし)。オフセットを伴います。
    • GC_ARRAY_START, GC_ARRAY_NEXT: 配列の走査用。
    • GC_CALL: サブルーチン呼び出し(GC情報内の別の命令列へのジャンプ)。
    • GC_MAP_PTR, GC_STRING, GC_EFACE, GC_IFACE, GC_SLICE: それぞれマップ、文字列、空インターフェース、非空インターフェース、スライスといった特定のGo組み込み型の処理用。
    • GC_REGION: オブジェクト内の特定の領域を走査するための命令。
    • GC_STACK_CAPACITY: GCが内部的に使用するスタックの最大深度を定義します。これは、深くネストされたデータ構造(例えば、多次元配列やネストされた構造体)を走査する際に、GCが再帰的に命令を処理するために必要です。
  4. テストケースの追加 (src/pkg/runtime/gc_test.go): TestGcDeepNesting という新しいテストが追加されました。このテストは、非常に深くネストされたポインタの配列([2][2][2][2][2][2][2][2][2][2]*int)を作成し、GCがそれを正しく処理できることを検証します。これは、新しいGC情報生成メカニズムが複雑な型構造を正確に扱えることを保証するために重要です。t.Logf("%p", a) は、コンパイラがエスケープ解析を適用してオブジェクトをスタックに割り当てるのを防ぎ、ヒープに割り当てられるようにするためのトリックです。

これらの変更により、GoのGCは、オブジェクトのメモリレイアウトを正確に理解し、ポインタのみを追跡することで、より効率的かつ正確なメモリ管理を実現できるようになりました。

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

このコミットのコアとなる変更は、主に以下の2つのファイルに集中しています。

  1. src/cmd/gc/reflect.c:

    • #include "../../pkg/runtime/mgc0.h" が追加され、GC命令の定義が利用可能になりました。
    • dgcsymdgcsym1 という2つの新しい静的関数が追加されました。これらが型からGC情報を生成する主要なロジックを含んでいます。
    • dcommontype 関数内の // gc コメントの行が変更され、duintptr(s, ot, 0) から dsymptr(s, ot, dgcsym(t), 0) へと変わりました。これは、型の共通情報にGC情報へのポインタを埋め込むことを意味します。
    • SymGcgen フラグが Sym 構造体に追加され、dgcsym 関数内でGC情報が既に生成されているかどうかのチェックに使用されます。
  2. src/pkg/runtime/mgc0.h:

    • このファイルは新規作成されました。
    • enum を使用して、GCがオブジェクトを走査する際に使用する様々な命令(オペコード)が定義されています。例: GC_END, GC_PTR, GC_APTR, GC_ARRAY_START, GC_SLICE, GC_REGION など。
    • GC_STACK_CAPACITY が定義されており、GCが深くネストされた構造体を処理する際に使用する内部スタックのサイズを示します。

コアとなるコードの解説

src/cmd/gc/reflect.cdgcsymdgcsym1

これらの関数は、Goコンパイラが各Goの型について、そのメモリレイアウト内のポインタの位置を記述する「GCプログラム」を生成する役割を担っています。

  • dgcsym(Type *t):

    • この関数は、特定の Type t のGC情報を生成するためのエントリポイントです。
    • まず、typesymprefix(".gc", t) を使って、その型に対応するGC情報シンボル(例: .gc.T)を取得します。
    • s->flags & SymGcgen をチェックして、そのシンボルに対してGC情報が既に生成されている場合は、重複を避けるために既存のシンボルを返します。
    • s->flags |= SymGcgen を設定し、GC情報が生成中であることをマークします。
    • ot = 0; off = 0; で出力オフセットと現在のメモリオフセットを初期化します。
    • ot = duintptr(s, ot, t->width); で、まず型の全体のサイズをGC情報に書き込みます。これは、GCがオブジェクトのサイズを知るために使用されます。
    • ot = dgcsym1(s, ot, t, &off, 0); を呼び出し、実際のGCプログラムの生成を dgcsym1 に委譲します。off は現在のメモリオフセットを追跡し、0 はGCスタックの初期深度です。
    • ot = duintptr(s, ot, GC_END); で、GCプログラムの終端を示す GC_END オペコードを書き込みます。
    • ggloblsym(s, ot, 1, 1); で、生成されたGCプログラムをグローバルシンボルとして出力します。
    • 最後に、生成されたGCプログラムのサイズと型の実際のサイズが一致するかどうかを検証するアサーション(fatal)が含まれています。これは、GC情報が型のメモリレイアウトを正確に反映していることを保証するための重要なチェックです。
  • dgcsym1(Sym *s, int ot, Type *t, vlong *off, int stack_size):

    • この関数は、dgcsym から再帰的に呼び出され、様々なGoの型を処理し、対応するGC命令を生成します。
    • switch(t->etype) を使用して、Goの様々な基本型(TINT8, TPTR32, TCHAN, TMAP, TSTRING, TINTER, TARRAY, TSTRUCT など)を処理します。
    • プリミティブ型(TINT8など): ポインタを含まないため、単に *off += t->width; でオフセットを進めるだけです。
    • ポインタ型(TPTR32, TPTR64:
      • GC_APTR (Arbitrary Pointer) または GC_PTR (Typed Pointer) オペコードを出力します。
      • haspointers(t->type) で、ポインタが指す型がさらにポインタを含むかどうかをチェックします。
      • GC_PTR の場合、ポインタが指す型のGC情報へのポインタも dsymptr で出力します。
    • マップ(TMAP: GC_MAP_PTR オペコードと、マップの型情報へのポインタを出力します。
    • 文字列(TSTRING: GC_STRING オペコードを出力します。Goの文字列は内部的にポインタと長さで構成されます。
    • インターフェース(TINTER: GC_EFACE (empty interface) または GC_IFACE (interface with methods) オペコードを出力します。インターフェースは内部的に型情報とデータへのポインタを持ちます。
    • 配列(TARRAY:
      • スライス(isslice(t))の場合、GC_SLICE オペコードと、スライスの要素型へのGC情報ポインタを出力します。
      • 固定長配列の場合、要素がポインタを含まない場合は単にオフセットを進めます。
      • 要素がポインタを含む場合、再帰的に dgcsym1 を呼び出して要素のGC情報を生成します。
      • 配列の要素数が多く、GCスタック容量を超える可能性がある場合、GC_REGION オペコードを使用して、その領域全体のGC情報を参照するようにします。これにより、深い再帰を避けます。
      • GC_ARRAY_STARTGC_ARRAY_NEXT オペコードは、GCが配列の各要素を効率的に走査するための命令です。
    • 構造体(TSTRUCT:
      • 構造体の各フィールドをループで走査し、各フィールドの型に対して再帰的に dgcsym1 を呼び出します。
      • これにより、構造体内の各フィールドのオフセットと、そのフィールドがポインタを含む場合のGC情報が生成されます。
    • fatal 関数は、予期しない型が渡された場合にコンパイラエラーを発生させます。

src/pkg/runtime/mgc0.h の GC 命令

このヘッダーファイルは、GCがオブジェクトを走査する際に解釈する「バイトコード」のような命令セットを定義しています。これらの命令は、GCがオブジェクトのメモリレイアウトを理解し、ポインタを正確に識別するために不可欠です。

  • enum: 各命令に一意の整数値を割り当てています。
  • コメント: 各命令の目的と、それに続く引数(オフセット off、オブジェクトGC情報 objgc、長さ len、要素サイズ elemsize、サイズ size など)が詳細に説明されています。
  • GC_STACK_CAPACITY: GCが深くネストされたデータ構造を処理する際に使用する内部スタックの最大深度を定義します。これは、GCが再帰的に命令を処理する際のバッファオーバーフローを防ぐための安全策です。

これらの変更により、Goコンパイラは、Goの型システムが持つ豊富な情報を活用して、ランタイムのGCがより効率的かつ正確に動作するための基盤を構築しました。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(当時のものがあれば)
  • Go言語のコンパイラとランタイムのソースコードリポジトリ
  • Gerrit Code Review: https://golang.org/cl/6290043

参考にした情報源リンク