[インデックス 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は、以下のステップで動作します。
- マーク (Mark): ルート(グローバル変数、スタック上の変数、レジスタなど)から到達可能なすべてのオブジェクトをマークします。
- スキャン (Scan): マークされたオブジェクトをたどり、そのオブジェクトが参照している他のオブジェクトもマークします。これを再帰的に行い、到達可能なすべてのオブジェクトを特定します。
- スイープ (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がこれを正確にスキャンすることが重要です。
Eface
とType
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
構造体を保守的にスキャンするのではなく、正確にスキャンできるようになります。
-
runtime·gc_g_ptr
関数の導入:src/pkg/runtime/mgc0.go
にgc_g_ptr
という新しいGo関数が追加されました。この関数は、C言語から呼び出されることを想定しており、*g
(Goroutine構造体へのポインタ)のGo型情報を返します。具体的には、*ret = (*g)(nil)
という行で、ret
ポインタが指すinterface{}
に*g
型のnil
値を設定します。これにより、ret
のtype
フィールドには*g
の型情報が格納されます。 -
runtime·gc_g_ptr
のC言語からの利用:src/pkg/runtime/malloc.h
にvoid runtime·gc_g_ptr(Eface*);
という宣言が追加され、Goで定義されたgc_g_ptr
関数がC言語から呼び出せるようになりました。 -
allocg
関数の導入とG
構造体の正確な割り当て:src/pkg/runtime/proc.c
にallocg
という新しい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に型情報を認識させる形でメモリを割り当てるランタイム関数です。
-
runtime·malg
からのallocg
の利用:src/pkg/runtime/proc.c
のruntime·malg
関数(Goroutineのメモリを割り当てる既存の関数)内で、newg = runtime·malloc(sizeof(G));
という行がnewg = allocg();
に置き換えられました。これにより、Goroutineの割り当てが、従来の単なるサイズベースのmalloc
から、正確な型情報に基づくallocg
関数を通じて行われるようになりました。
これらの変更により、GCはG
構造体の内部構造を正確に理解し、ポインタと非ポインタを区別できるようになります。これにより、GCの精度が向上し、不要なメモリの保持(浮動ゴミ)が減少し、全体的なメモリ効率とGC性能が改善されます。
schedlink
ポインタのゼロクリアについては、このコミットのdiffには直接的なコード変更が見られませんが、コミットメッセージに明記されているため、Goroutineがスケジューラのキューから外れる際に、そのポインタが明示的にnil
に設定されるようなロジックが、このコミットの一部として、あるいは関連する変更として行われたことを示唆しています。これは、GCが不要な参照を追跡しないようにするための重要なクリーンアップです。
コアとなるコードの変更箇所
このコミットでは、以下の3つのファイルが変更されています。
-
src/pkg/runtime/malloc.h
:runtime·gc_g_ptr
関数のC言語からの宣言が追加されました。
-
src/pkg/runtime/mgc0.go
:- Go言語で
gc_g_ptr
関数が新しく定義されました。この関数は、*g
型(Goroutineへのポインタ型)の情報をC言語側に提供します。
- Go言語で
-
src/pkg/runtime/proc.c
:- 新しいC言語関数
allocg
が追加されました。この関数は、gc_g_ptr
から取得した正確な型情報に基づいてG
構造体を割り当てます。 - 既存の
runtime·malg
関数内で、G
構造体の割り当てがruntime·malloc(sizeof(G))
からallocg()
の呼び出しに置き換えられました。
- 新しいC言語関数
コアとなるコードの解説
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言語のランタイムコードから呼び出されることを意図しています。引数ret
はinterface{}
型へのポインタです。この関数は、*ret = (*g)(nil)
という行によって、ret
が指すインターフェース値に*g
型(Goroutine構造体へのポインタ型)のnil
値を設定します。これにより、C言語側はret
のtype
フィールドを通じて、*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のメモリをより効率的かつ正確に管理するための重要なステップです。
関連リンク
- Go言語公式ウェブサイト: https://golang.org/
- Go言語のソースコード (GitHub): https://github.com/golang/go
- Go言語のIssueトラッカー: https://github.com/golang/go/issues
参考にした情報源リンク
- コミットメッセージとdiff情報:
./commit_data/19250.txt
- Go言語のガベージコレクションに関する一般的な知識
- Goランタイムの内部構造に関する一般的な知識
- Issue #7820については、公開されているGoのIssueトラッカーでは直接該当する情報を見つけることができませんでした。
- Go Code Review Comments (CL 89360043): https://golang.org/cl/89360043 (これはコミットメッセージに記載されているChangeListへのリンクであり、追加の情報源として参照しました。)