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

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

このコミットは、GoランタイムにおけるGoroutine (G) のスキャン方法を改善し、ガベージコレクション (GC) の精度と効率を向上させることを目的としています。具体的には、GCがG構造体を保守的にスキャンするのではなく、正確な型情報に基づいてスキャンするように変更し、不要になったschedlinkポインタをゼロクリアすることで、GCの動作をよりクリーンにしています。

コミット

commit 29d1b211fdece82b3c4ccdb549ac394e71132643
Author: Keith Randall <khr@golang.org>
Date:   Mon Apr 28 12:47:09 2014 -0400

    runtime: clean up scanning of Gs

    Use a real type for Gs instead of scanning them conservatively.
    Zero the schedlink pointer when it is dead.

    Update #7820

    LGTM=rsc
    R=rsc, dvyukov
    CC=golang-codereviews
    https://golang.org/cl/89360043

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

https://github.com/golang/go/commit/29d1b211fdece82b3c4ccdb549ac394e71132643

元コミット内容

Goランタイムにおいて、Goroutine (G) のスキャン処理を整理し、改善します。 具体的には、G構造体を保守的にスキャンするのではなく、実際の型情報を使用してスキャンするように変更します。また、不要になったschedlinkポインタをゼロクリアします。

変更の背景

Goのガベージコレクタは、メモリ上のオブジェクトを正確に識別し、到達可能なオブジェクトをマークすることで、不要なメモリを解放します。このプロセスにおいて、GCはオブジェクトの型情報に基づいて、どのフィールドがポインタであり、どのフィールドが非ポインタであるかを判断する必要があります。

このコミット以前は、GoランタイムのG (Goroutine) 構造体が、GCによって「保守的に」スキャンされていたと考えられます。保守的なスキャンとは、GCが特定のメモリ領域の正確な型情報を持たない場合に、その領域内のすべてのワードがポインタである可能性を考慮してスキャンする方式です。これは、誤ってポインタでないものをポインタと解釈し、到達不能なオブジェクトを解放しない「浮動ゴミ (floating garbage)」を発生させる可能性があります。結果として、メモリ使用量の増加やGC効率の低下を招くことがあります。

このコミットの目的は、G構造体に対して正確な型情報を提供することで、GCがGを「正確に」スキャンできるようにすることです。これにより、GCはG構造体内のどのフィールドがポインタであるかを正確に認識し、より効率的かつ正確なメモリ管理が可能になります。

また、schedlinkポインタのゼロクリアは、Goroutineがスケジューラのキューから外れた(つまり「デッド」になった)際に、そのポインタを明示的にnilに設定することで、GCが誤ってそのポインタを有効な参照と見なすことを防ぎ、浮動ゴミの発生を抑制するクリーンアップです。

コミットメッセージに記載されているUpdate #7820については、公開されているGoのIssueトラッカーでは直接該当するIssueを見つけることができませんでした。これは、古いIssueであるため番号が変更されたか、内部的なIssueトラッカーを参照している可能性がありますが、変更内容から推測するに、GCの精度向上とメモリ管理の最適化に関する課題が背景にあったと考えられます。

前提知識の解説

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

Goは、トレース型ガベージコレクタを採用しています。これは、プログラムが使用しているオブジェクト(到達可能なオブジェクト)を特定し、それ以外のオブジェクト(到達不能なオブジェクト、つまりゴミ)を解放する方式です。GCは、以下のステップで動作します。

  1. マーク (Mark): ルート(グローバル変数、スタック上の変数、レジスタなど)から到達可能なすべてのオブジェクトをマークします。
  2. スキャン (Scan): マークされたオブジェクトをたどり、そのオブジェクトが参照している他のオブジェクトもマークします。これを再帰的に行い、到達可能なすべてのオブジェクトを特定します。
  3. スイープ (Sweep): マークされなかったオブジェクト(ゴミ)を解放し、メモリを再利用可能にします。

保守的GCと正確なGC

  • 保守的GC (Conservative GC): メモリ上の値がポインタであるかどうかの確実な情報がない場合でも、それがポインタである可能性があればポインタとして扱います。これにより、誤って生きているオブジェクトを解放してしまうことを防ぎますが、実際にはゴミであるオブジェクトを解放できない「浮動ゴミ」を生み出す可能性があります。
  • 正確なGC (Precise GC): オブジェクトの型情報に基づいて、どのフィールドがポインタであり、どのフィールドが非ポインタであるかを正確に識別します。これにより、GCはより効率的かつ正確にメモリを管理でき、浮動ゴミの発生を最小限に抑えることができます。GoのGCは、基本的に正確なGCを目指していますが、一部の領域では保守的なスキャンが用いられることもあります。

Goroutine (G) 構造体

Goランタイムにおいて、g構造体は個々のGoroutineを表します。この構造体には、Goroutineのスタック情報、現在の状態、スケジューラ関連のリンク(schedlinkなど)、およびその他のGoroutine固有のデータが含まれています。g構造体自体もメモリ上に割り当てられるオブジェクトであり、GCの対象となります。g構造体には、他のメモリ領域へのポインタ(例: スタックポインタ)が含まれるため、GCがこれを正確にスキャンすることが重要です。

EfaceType

Goのランタイムでは、Goの型情報をCコードから利用するために、Eface (Empty Interface) やType構造体が用いられます。

  • Efaceは、Goのinterface{}型をC言語側で表現するための構造体で、通常はtypeポインタとdataポインタを含みます。typeポインタは、格納されている値の型情報へのポインタです。
  • Typeは、Goの型システムにおける型情報を表す構造体です。PtrTypeはその一種で、ポインタ型を表し、そのelemフィールドはポインタが指す要素の型情報を含みます。

これらのメカニズムを通じて、C言語で書かれたランタイムコードが、Goの型システムと連携し、正確なメモリ管理やGCの動作を実現します。

技術的詳細

このコミットの主要な技術的変更点は、GoランタイムがGoroutine (G構造体) を割り当てる際に、その正確な型情報をGCに提供するようにしたことです。これにより、GCはG構造体を保守的にスキャンするのではなく、正確にスキャンできるようになります。

  1. runtime·gc_g_ptr関数の導入: src/pkg/runtime/mgc0.gogc_g_ptrという新しいGo関数が追加されました。この関数は、C言語から呼び出されることを想定しており、*g(Goroutine構造体へのポインタ)のGo型情報を返します。具体的には、*ret = (*g)(nil)という行で、retポインタが指すinterface{}*g型のnil値を設定します。これにより、rettypeフィールドには*gの型情報が格納されます。

  2. runtime·gc_g_ptrのC言語からの利用: src/pkg/runtime/malloc.hvoid runtime·gc_g_ptr(Eface*);という宣言が追加され、Goで定義されたgc_g_ptr関数がC言語から呼び出せるようになりました。

  3. allocg関数の導入とG構造体の正確な割り当て: src/pkg/runtime/proc.callocgという新しいC関数が導入されました。この関数は、新しいG構造体を割り当てる責任を持ちます。

    • if(gtype == nil)ブロック内で、runtime·gc_g_ptr(&e)を呼び出し、Eface eを通じて*gのGo型情報を取得します。
    • 取得したEfaceから、gtype = ((PtrType*)e.type)->elem;という行で、*gが指す実際の型(つまりg構造体そのものの型)を取得します。このgtypeは静的変数gtypeにキャッシュされ、以降の呼び出しで再利用されます。
    • 最後に、gp = runtime·cnew(gtype);を呼び出して、取得した正確な型情報gtypeに基づいてG構造体のメモリを割り当てます。runtime·cnewは、GCに型情報を認識させる形でメモリを割り当てるランタイム関数です。
  4. runtime·malgからのallocgの利用: src/pkg/runtime/proc.cruntime·malg関数(Goroutineのメモリを割り当てる既存の関数)内で、newg = runtime·malloc(sizeof(G));という行がnewg = allocg();に置き換えられました。これにより、Goroutineの割り当てが、従来の単なるサイズベースのmallocから、正確な型情報に基づくallocg関数を通じて行われるようになりました。

これらの変更により、GCはG構造体の内部構造を正確に理解し、ポインタと非ポインタを区別できるようになります。これにより、GCの精度が向上し、不要なメモリの保持(浮動ゴミ)が減少し、全体的なメモリ効率とGC性能が改善されます。

schedlinkポインタのゼロクリアについては、このコミットのdiffには直接的なコード変更が見られませんが、コミットメッセージに明記されているため、Goroutineがスケジューラのキューから外れる際に、そのポインタが明示的にnilに設定されるようなロジックが、このコミットの一部として、あるいは関連する変更として行われたことを示唆しています。これは、GCが不要な参照を追跡しないようにするための重要なクリーンアップです。

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

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

  1. src/pkg/runtime/malloc.h:

    • runtime·gc_g_ptr関数のC言語からの宣言が追加されました。
  2. src/pkg/runtime/mgc0.go:

    • Go言語でgc_g_ptr関数が新しく定義されました。この関数は、*g型(Goroutineへのポインタ型)の情報をC言語側に提供します。
  3. src/pkg/runtime/proc.c:

    • 新しいC言語関数allocgが追加されました。この関数は、gc_g_ptrから取得した正確な型情報に基づいてG構造体を割り当てます。
    • 既存のruntime·malg関数内で、G構造体の割り当てがruntime·malloc(sizeof(G))からallocg()の呼び出しに置き換えられました。

コアとなるコードの解説

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

// Called from C. Returns the Go type *g.
func gc_g_ptr(ret *interface{}) {
	*ret = (*g)(nil)
}

このGo関数gc_g_ptrは、C言語のランタイムコードから呼び出されることを意図しています。引数retinterface{}型へのポインタです。この関数は、*ret = (*g)(nil)という行によって、retが指すインターフェース値に*g型(Goroutine構造体へのポインタ型)のnil値を設定します。これにより、C言語側はrettypeフィールドを通じて、*gの正確な型情報を取得できるようになります。これは、GCがg構造体のメモリレイアウトを正確に理解するために不可欠な情報です。

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

static G*
allocg(void)
{
	G *gp;
	static Type *gtype; // G構造体の型情報をキャッシュするための静的変数

	if(gtype == nil) { // 初回呼び出し時のみ型情報を取得
		Eface e;
		runtime·gc_g_ptr(&e); // Go関数gc_g_ptrを呼び出し、*gの型情報をEface eに取得
		gtype = ((PtrType*)e.type)->elem; // Efaceからポインタ型(*g)の要素型(g構造体そのもの)を取得
	}
	gp = runtime·cnew(gtype); // 取得した正確な型情報に基づいてG構造体を割り当てる
	return gp;
}

このC関数allocgは、新しいGoroutine (G構造体) を割り当てるためのヘルパー関数です。

  • static Type *gtype;は、g構造体の型情報を一度取得したらキャッシュしておくための静的変数です。
  • if(gtype == nil)ブロックは、gtypeがまだ初期化されていない場合にのみ実行されます。
  • runtime·gc_g_ptr(&e);は、Goで定義されたgc_g_ptr関数を呼び出し、*gの型情報をEface eに格納します。
  • gtype = ((PtrType*)e.type)->elem;は、Eface eから取得した型情報がポインタ型(*g)であると仮定し、そのポインタが指す要素の型(つまり、g構造体そのものの型)を抽出してgtypeに設定します。
  • gp = runtime·cnew(gtype);は、Goランタイムのメモリ割り当て関数で、引数として渡された型情報に基づいてメモリを割り当てます。これにより、GCは割り当てられたG構造体の正確なレイアウトを認識し、ポインタフィールドを正確にスキャンできるようになります。
// runtime·malg関数内の変更
// 変更前: newg = runtime·malloc(sizeof(G));
// 変更後: newg = allocg();

runtime·malg関数は、新しいGoroutineのスタックとG構造体を割り当てる役割を担っています。この変更により、G構造体の割り当てが、単にG構造体のサイズ分のメモリを確保するruntime·mallocから、新しく導入されたallocg関数を通じて行われるようになりました。allocgは、前述の通り、G構造体の正確な型情報に基づいてメモリを割り当てるため、GCの精度向上に直接貢献します。

これらの変更は、GoランタイムのGCがGoroutineのメモリをより効率的かつ正確に管理するための重要なステップです。

関連リンク

参考にした情報源リンク

  • コミットメッセージとdiff情報: ./commit_data/19250.txt
  • Go言語のガベージコレクションに関する一般的な知識
  • Goランタイムの内部構造に関する一般的な知識
  • Issue #7820については、公開されているGoのIssueトラッカーでは直接該当する情報を見つけることができませんでした。
  • Go Code Review Comments (CL 89360043): https://golang.org/cl/89360043 (これはコミットメッセージに記載されているChangeListへのリンクであり、追加の情報源として参照しました。)