[インデックス 13216] ファイルの概要
このコミットは、Go言語のランタイムにおける複数のファイルにわたる変更を含んでいます。主な変更は、ガベージコレクション(GC)の将来的な変更に備えて、内部的なデータ構造のフィールド型を更新することです。具体的には、src/pkg/runtime/cgocall.c, src/pkg/runtime/mgc0.c, src/pkg/runtime/mprof.goc, src/pkg/runtime/proc.c, src/pkg/runtime/runtime.h, および各種スレッド関連ファイル (thread_darwin.c など) とトレースバック関連ファイル (traceback_arm.c, traceback_x86.c) が影響を受けています。
コミット
このコミットは、Goランタイムの内部フィールド型を更新し、特にポインタ型 (byte*) を符号なし整数型 (uintptr) に変更することで、今後のガベージコレクションの改善に備えるものです。これにより、GCがメモリをより効率的かつ安全に管理できるよう、内部的なアドレス表現が統一されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/334bf95f9e66a1751692c0bdcee2c03183d89375
元コミット内容
runtime: update field types in preparation for GC changes
R=rsc, remyoudompheng, minux.ma, ality
CC=golang-dev
https://golang.org/cl/6242061
---
src/pkg/runtime/cgocall.c | 2 +-\
src/pkg/runtime/mgc0.c | 12 +++----\
src/pkg/runtime/mprof.goc | 2 +-\
src/pkg/runtime/proc.c | 68 +++++++++++++++++++++-------------------\
src/pkg/runtime/runtime.h | 16 +++++-----\
src/pkg/runtime/thread_darwin.c | 2 +-\
src/pkg/runtime/thread_freebsd.c | 2 +-\
src/pkg/runtime/thread_linux.c | 2 +-\
src/pkg/runtime/thread_netbsd.c | 2 +-\
src/pkg/runtime/thread_openbsd.c | 2 +-\
src/pkg/runtime/traceback_arm.c | 6 ++--\
src/pkg/runtime/traceback_x86.c | 10 +++---\
12 files changed, 65 insertions(+), 61 deletions(-)
変更の背景
このコミットの主な背景は、Goランタイムのガベージコレクション(GC)メカニズムの進化にあります。2012年当時のGoのGCは、現在のような並行・低遅延GCとは異なり、よりシンプルなマーク&スイープ方式でした。GCが効率的かつ正確に動作するためには、メモリ上のオブジェクトを正確に識別し、到達可能なオブジェクトとそうでないオブジェクトを区別する必要があります。
Goのランタイムは、ゴルーチン(goroutine)のスタックや内部データ構造に、メモリ上のアドレスを指すポインタを多数保持しています。これらのポインタの中には、Goのオブジェクトを直接指すもの(GCが追跡すべきもの)と、単なるメモリ上の位置を示す数値として扱われるもの(GCが追跡する必要がない、あるいは追跡してはならないもの)があります。
byte* のようなC言語スタイルのポインタ型は、コンパイラに対してその値がメモリ上のアドレスであり、デリファレンス(間接参照)可能であることを示唆します。しかし、ランタイムの内部処理では、アドレスを数値として扱いたい場合や、GCが誤って追跡しないようにしたい場合があります。
このコミットは、GCがメモリをスキャンする際に、どの値が実際のGoオブジェクトへのポインタであり、どの値が単なる数値としてのアドレスであるかをより明確に区別できるようにするための準備です。uintptr 型への変更は、これらのフィールドが「ポインタとしてデリファレンスされるべきではないが、アドレスとして扱われるべき数値」であることを明示し、GCの正確性と堅牢性を向上させることを目的としています。これにより、GCはより安全にメモリを走査し、不要なオブジェクトを解放できるようになります。
前提知識の解説
Goランタイムの役割
Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネル通信、システムコールインターフェースなどが含まれます。Goプログラムは、コンパイル時にランタイムとリンクされ、ランタイムが提供する機能を利用して動作します。ランタイムはC言語とGo言語(当時は主にC言語に近いGoのサブセット)で実装されており、OSとの直接的なやり取りや、Go言語の並行処理モデルの実現を担っています。
Goのガベージコレクション(GC)の基本
ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはや使用されていない(到達不可能になった)ものを自動的に解放する仕組みです。GoのGCは、マーク&スイープ方式を基本としています。
- マークフェーズ: GCは、プログラムの実行を一時停止(ストップ・ザ・ワールド)し、ルート(グローバル変数、実行中のゴルーチンのスタック、レジスタなど)から到達可能なすべてのオブジェクトをマークします。
- スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが占めるメモリ領域を解放し、再利用可能にします。
このコミットが行われた2012年頃のGoのGCは、ストップ・ザ・ワールド時間が比較的長くなる傾向がありました。そのため、GCの効率と正確性を高めるための改善が継続的に行われていました。ポインタの正確な識別は、GCがメモリグラフを正しく辿る上で極めて重要です。
byte* と uintptr の違いと使い分け
-
byte*(C言語スタイルのポインタ): C言語におけるbyte*は、1バイトのデータ型 (byteまたはunsigned char) へのポインタです。これはメモリ上の特定のアドレスを指し、そのアドレスにあるデータを読み書きするためにデリファレンス(*ptr)することができます。C言語のコンパイラは、byte*をポインタとして扱い、ポインタ演算(ptr + 1など)を行う際に、指している型のサイズ(この場合は1バイト)を考慮します。GoランタイムのCコードでは、メモリブロックの先頭アドレスや、型が不明な汎用的なメモリ領域を指すためによく使われていました。 -
uintptr(Go言語の符号なし整数型):uintptrは、ポインタのビットパターンを保持できるだけの大きさを持つ符号なし整数型です。これはGo言語の組み込み型であり、ポインタとは異なり、直接デリファレンスすることはできません。uintptrは、メモリ上のアドレスを数値として扱う場合に用いられます。例えば、アドレスの計算、アライメントの調整、あるいはGCが追跡すべきポインタではないことを明示する場合などに使用されます。uintptrは、Goのポインタ型 (*T) との間で明示的な型変換を行うことができますが、これは危険な操作であり、GCの追跡対象から外れる可能性があるため、ランタイムのような低レベルコードでのみ慎重に利用されます。
このコミットでは、byte* から uintptr への変更は、これらのフィールドが「メモリ上のアドレスを表す数値」であり、「GoのGCが追跡すべきオブジェクトへのポインタではない」ことを明確にする意図があります。これにより、GCはこれらの値をポインタとしてスキャンせず、純粋な数値として扱うことができます。
Goにおけるスタック管理
Goのゴルーチンは、それぞれ独立したスタックを持っています。Goのスタックは、必要に応じて動的にサイズが変更される「可変長スタック」です。関数呼び出しがスタックの現在の容量を超えそうになると、ランタイムはより大きな新しいスタックセグメントを割り当て、古いスタックの内容を新しいスタックにコピーします(morestack)。関数から戻る際にスタックが過剰に大きい場合は、より小さなスタックに縮小されることもあります(lessstack)。
スタックの境界(stackguard, stackbase)は、スタックオーバーフローを検出したり、GCがスタックをスキャンする範囲を決定したりするために重要です。これらの境界はメモリ上のアドレスで表現されるため、その型がGCの動作に影響を与えます。
G および Gobuf 構造体の役割
-
G構造体: GoランタイムにおけるG構造体は、個々のゴルーチンを表します。この構造体には、ゴルーチンの状態(実行中、待機中など)、スタックの境界 (stackguard,stackbase)、スケジューリング情報 (sched)、パニック情報 (panic) など、ゴルーチンに関するあらゆる重要な情報が格納されています。GCは、G構造体を通じて各ゴルーチンのスタックをスキャンし、到達可能なオブジェクトを特定します。 -
Gobuf構造体:Gobuf構造体は、ゴルーチンの実行コンテキスト(プログラムカウンタpc、スタックポインタsp、現在のゴルーチンg)を保存および復元するために使用されます。これは、ゴルーチンの切り替え(コンテキストスイッチ)や、システムコールへの出入り、スタックの拡張/縮小などの際に、ゴルーチンの実行状態を一時的に保存するために利用されます。
技術的詳細
このコミットの技術的な核心は、Goランタイムの内部データ構造におけるポインタ表現の厳密化と、それによるガベージコレクションの効率化および正確性の向上です。
GoのGCは、メモリ上の値をスキャンして、それがGoオブジェクトへのポインタであるかどうかを判断する必要があります。もし、単なる数値としてのアドレスを誤ってポインタとして解釈し、そのアドレスが不正なメモリ領域を指していた場合、GCはクラッシュしたり、誤ったメモリを解放したりする可能性があります。逆に、Goオブジェクトへの有効なポインタを数値として扱ってしまった場合、そのオブジェクトは到達不可能と判断され、誤って解放されてしまう(use-after-freeバグを引き起こす)可能性があります。
byte* から uintptr への型変更は、以下の目的を持っています。
- GCスキャンの最適化:
uintptr型のフィールドは、GCがポインタとして追跡する必要がないことを明示します。これにより、GCはこれらのフィールドをスキップし、スキャン対象を実際のGoオブジェクトポインタに限定できます。これは、GCのパフォーマンス向上に寄与します。 - メモリ安全性の向上: ランタイム内部でアドレスを数値として扱う場合、
uintptrを使用することで、コンパイラがポインタのデリファレンスを許可しないため、意図しないデリファレンスによるクラッシュを防ぐことができます。これは、ランタイムの堅牢性を高めます。 - セマンティクスの明確化:
uintptrを使用することで、そのフィールドが「メモリ上のアドレスを表す数値」であり、「Goの型システムが管理するオブジェクトへのポインタ」ではないというセマンティクスが明確になります。これにより、コードの可読性と保守性が向上します。
具体的に変更されたフィールドを見てみましょう。
-
Gobuf構造体内のspフィールド:sp(スタックポインタ) は、スタック上の特定の位置を指すアドレスです。GCはスタック全体をスキャンしますが、sp自体がGoオブジェクトへのポインタである必要はありません。uintptrにすることで、spが純粋なアドレス値として扱われ、GCがその値を追跡対象から外すことができます。 -
G構造体内のスタック関連フィールド (stackguard,stackbase,gcstack,gcsp,gcguard,stack0): これらのフィールドは、ゴルーチンのスタックの境界や、GC中に使用されるスタック関連のアドレスを保持します。これらはメモリ上の位置を示す数値であり、Goオブジェクトへのポインタではありません。uintptrに変更することで、GCがこれらの値をポインタとして誤って解釈することを防ぎ、スタックのスキャン範囲を正確に特定できるようになります。 -
Defer構造体内のargsフィールド:Defer構造体はdeferステートメントの情報を保持します。argsフィールドは、deferされた関数の引数を格納するための領域です。以前はbyte args[8]のように固定サイズのバイト配列でしたが、void* args[1]に変更され、さらにmallocsizの計算ロジックが追加されています。これは、引数のサイズが可変であること、そしてその領域がGCが直接追跡するGoオブジェクトへのポインタではなく、単なる生データ領域であることを示唆しています。void*はC言語における汎用ポインタであり、Goのランタイム内部で型安全性を犠牲にしてメモリを操作する際に用いられます。
これらの変更は、GoのGCがより洗練され、並行GCなどの高度な機能が導入されるための基盤を築くものでした。GCは、ポインタと非ポインタを正確に区別できる「ポインタマップ」のような情報を必要としますが、ランタイムの内部データ構造で uintptr を使用することは、この区別をより容易にする一歩となります。
コアとなるコードの変更箇所
このコミットの最も重要な変更は、src/pkg/runtime/runtime.h における構造体定義の変更です。
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -167,7 +167,7 @@ struct Slice
struct Gobuf
{
// The offsets of these fields are known to (hard-coded in) libmach.
- byte* sp;
+ uintptr sp;
byte* pc;
G* g;
};
@@ -183,15 +183,15 @@ struct GCStats
};
struct G
{
- byte* stackguard; // cannot move - also known to linker, libmach, runtime/cgo
- byte* stackbase; // cannot move - also known to libmach, runtime/cgo
+ uintptr stackguard; // cannot move - also known to linker, libmach, runtime/cgo
+ uintptr stackbase; // cannot move - also known to libmach, runtime/cgo
Defer* defer;
Panic* panic;
Gobuf sched;
- byte* gcstack; // if status==Gsyscall, gcstack = stackbase to use during gc
- byte* gcsp; // if status==Gsyscall, gcsp = sched.sp to use during gc
- byte* gcguard; // if status==Gsyscall, gcguard = stackguard to use during gc
- byte* stack0;
+ uintptr gcstack; // if status==Gsyscall, gcstack = stackbase to use during gc
+ uintptr gcsp; // if status==Gsyscall, gcsp = sched.sp to use during gc
+ uintptr gcguard; // if status==Gsyscall, gcguard = stackguard to use during gc
+ uintptr stack0;
byte* entry; // initial function
G* alllink; // on allg
void* param; // passed parameter on wakeup
@@ -486,7 +486,7 @@ struct Defer
byte* pc;
byte* fn;
Defer* link;
- byte args[8]; // padded to actual size
+ void* args[1]; // padded to actual size
};
その他のファイルでの変更例
src/pkg/runtime/proc.c や src/pkg/runtime/mgc0.c など、ランタイムのCコードでは、上記 runtime.h で型が変更されたフィールドへのアクセス箇所で、明示的な型キャストが多数追加されています。
例: src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -318,7 +318,7 @@ runtime·tracebackothers(G *me)\
continue;\
runtime·printf("\n");\
runtime·goroutineheader(g);\
- runtime·traceback(g->sched.pc, g->sched.sp, 0, g);\
+ runtime·traceback(g->sched.pc, (byte*)g->sched.sp, 0, g);\
}\
}\
ここでは g->sched.sp が uintptr に変更されたため、runtime·traceback 関数に渡す際に (byte*) へキャストしています。これは、runtime·traceback が依然として byte* 型の引数を期待しているためです。
コアとなるコードの解説
runtime.h の変更
-
Gobuf.sp:Gobufはゴルーチンのコンテキストを保存する構造体です。spフィールドはスタックポインタを保持します。これをbyte*からuintptrに変更することで、spが純粋なメモリアドレスの数値表現として扱われるようになります。これにより、GCがこのフィールドをポインタとして追跡するのを防ぎ、GCの正確性とパフォーマンスを向上させます。 -
G構造体のスタック関連フィールド:stackguard,stackbase,gcstack,gcsp,gcguard,stack0はすべて、ゴルーチンのスタックの境界や、GC中に使用されるスタック関連のアドレスを定義するものです。これらをbyte*からuintptrに変更することは、これらのフィールドがGoオブジェクトへのポインタではなく、単なるメモリ上の位置を示す数値であることを明確にします。これにより、GCはこれらの値をスキャン対象から除外し、スタックの正確な範囲を数値として計算できるようになります。これは、GCがスタックをスキャンする際の効率と安全性を高める上で非常に重要です。 -
Defer.args:Defer構造体はdeferステートメントの情報を保持します。argsフィールドは、deferされた関数の引数を格納するための領域です。byte args[8]からvoid* args[1]への変更は、引数のサイズが固定ではなく、可変であることを示唆しています。void*はC言語における汎用ポインタであり、Goのランタイム内部で型安全性を犠牲にしてメモリを操作する際に用いられます。この変更は、deferの引数処理をより柔軟にするためのものであり、GCがこの領域を直接ポインタとして追跡するのではなく、必要に応じて内部的に処理することを意図しています。また、proc.cのruntime·deferproc関数でmallocsizの計算ロジックが追加されており、引数の実際のサイズに基づいてメモリを動的に確保するようになっています。
その他のファイルでのキャストの追加
runtime.h で型が変更されたことにより、cgocall.c, mgc0.c, mprof.goc, proc.c, traceback_arm.c, traceback_x86.c などのファイルでは、これらのフィールドを使用する際に明示的な型キャスト ((byte*) や (uintptr)) が追加されています。これは、C言語の型システムが厳密であるため、新しい uintptr 型の値を、以前 byte* を期待していた関数や操作に渡す際に必要となります。これらのキャストは、ランタイムの内部ロジックが引き続き正しく機能するようにするための適応であり、GCの変更に向けた準備の一環として行われています。
これらの変更は、Goのランタイムがメモリをより低レベルで、かつGCにとってより理解しやすい形で管理するための重要なステップでした。
関連リンク
- Go言語公式ドキュメント: https://golang.org/doc/
- Go言語のランタイムソースコード (GitHub): https://github.com/golang/go/tree/master/src/runtime
- Goのガベージコレクションに関するブログ記事やドキュメント(当時の状況を理解するために、古い情報源も参照すると良いでしょう)
参考にした情報源リンク
- Go言語の
uintptr型に関する公式ドキュメントや解説記事 - Go言語のガベージコレクションの歴史と進化に関する技術ブログや論文
- C言語におけるポインタと型変換に関する一般的な情報
- Goのランタイムソースコードの関連部分(特に
src/pkg/runtime/ディレクトリ) - GoのIssueトラッカーやメーリングリストでの関連議論(
golang.org/cl/6242061など)
[インデックス 13216] ファイルの概要
このコミットは、Go言語のランタイムにおける複数のファイルにわたる変更を含んでいます。主な変更は、ガベージコレクション(GC)の将来的な変更に備えて、内部的なデータ構造のフィールド型を更新することです。具体的には、src/pkg/runtime/cgocall.c, src/pkg/runtime/mgc0.c, src/pkg/runtime/mprof.goc, src/pkg/runtime/proc.c, src/pkg/runtime/runtime.h, および各種スレッド関連ファイル (thread_darwin.c など) とトレースバック関連ファイル (traceback_arm.c, traceback_x86.c) が影響を受けています。
コミット
このコミットは、Goランタイムの内部フィールド型を更新し、特にポインタ型 (byte*) を符号なし整数型 (uintptr) に変更することで、今後のガベージコレクションの改善に備えるものです。これにより、GCがメモリをより効率的かつ安全に管理できるよう、内部的なアドレス表現が統一されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/334bf95f9e66a1751692c0bdcee2c03183d89375
元コミット内容
runtime: update field types in preparation for GC changes
R=rsc, remyoudompheng, minux.ma, ality
CC=golang-dev
https://golang.org/cl/6242061
---
src/pkg/runtime/cgocall.c | 2 +-\
src/pkg/runtime/mgc0.c | 12 +++----\
src/pkg/runtime/mprof.goc | 2 +-\
src/pkg/runtime/proc.c | 68 +++++++++++++++++++++-------------------\
src/pkg/runtime/runtime.h | 16 +++++-----\
src/pkg/runtime/thread_darwin.c | 2 +-\
src/pkg/runtime/thread_freebsd.c | 2 +-\
src/pkg/runtime/thread_linux.c | 2 +-\
src/pkg/runtime/thread_netbsd.c | 2 +-\
src/pkg/runtime/thread_openbsd.c | 2 +-\
src/pkg/runtime/traceback_arm.c | 6 ++--\
src/pkg/runtime/traceback_x86.c | 10 +++---\
12 files changed, 65 insertions(+), 61 deletions(-)
変更の背景
このコミットの主な背景は、Goランタイムのガベージコレクション(GC)メカニズムの進化にあります。2012年当時のGoのGCは、現在のような並行・低遅延GCとは異なり、よりシンプルな「ストップ・ザ・ワールド」(STW)方式のマーク&スイープコレクタでした。GCが効率的かつ正確に動作するためには、メモリ上のオブジェクトを正確に識別し、到達可能なオブジェクトとそうでないオブジェクトを区別する必要があります。
Goのランタイムは、ゴルーチン(goroutine)のスタックや内部データ構造に、メモリ上のアドレスを指すポインタを多数保持しています。これらのポインタの中には、Goのオブジェクトを直接指すもの(GCが追跡すべきもの)と、単なるメモリ上の位置を示す数値として扱われるもの(GCが追跡する必要がない、あるいは追跡してはならないもの)があります。
byte* のようなC言語スタイルのポインタ型は、コンパイラに対してその値がメモリ上のアドレスであり、デリファレンス(間接参照)可能であることを示唆します。しかし、ランタイムの内部処理では、アドレスを数値として扱いたい場合や、GCが誤って追跡しないようにしたい場合があります。
このコミットは、GCがメモリをスキャンする際に、どの値が実際のGoオブジェクトへのポインタであり、どの値が単なる数値としてのアドレスであるかをより明確に区別できるようにするための準備です。uintptr 型への変更は、これらのフィールドが「ポインタとしてデリファレンスされるべきではないが、アドレスとして扱われるべき数値」であることを明示し、GCの正確性と堅牢性を向上させることを目的としています。これにより、GCはより安全にメモリを走査し、不要なオブジェクトを解放できるようになります。
前提知識の解説
Goランタイムの役割
Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネル通信、システムコールインターフェースなどが含まれます。Goプログラムは、コンパイル時にランタイムとリンクされ、ランタイムが提供する機能を利用して動作します。ランタイムはC言語とGo言語(当時は主にC言語に近いGoのサブセット)で実装されており、OSとの直接的なやり取りや、Go言語の並行処理モデルの実現を担っています。
Goのガベージコレクション(GC)の基本
ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはや使用されていない(到達不可能になった)ものを自動的に解放する仕組みです。GoのGCは、マーク&スイープ方式を基本としています。
- マークフェーズ: GCは、プログラムの実行を一時停止(ストップ・ザ・ワールド)し、ルート(グローバル変数、実行中のゴルーチンのスタック、レジスタなど)から到達可能なすべてのオブジェクトをマークします。
- スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが占めるメモリ領域を解放し、再利用可能にします。
このコミットが行われた2012年頃のGoのGCは、ストップ・ザ・ワールド時間が比較的長くなる傾向がありました。そのため、GCの効率と正確性を高めるための改善が継続的に行われていました。ポインタの正確な識別は、GCがメモリグラフを正しく辿る上で極めて重要です。
byte* と uintptr の違いと使い分け
-
byte*(C言語スタイルのポインタ): C言語におけるbyte*は、1バイトのデータ型 (byteまたはunsigned char) へのポインタです。これはメモリ上の特定のアドレスを指し、そのアドレスにあるデータを読み書きするためにデリファレンス(*ptr)することができます。C言語のコンパイラは、byte*をポインタとして扱い、ポインタ演算(ptr + 1など)を行う際に、指している型のサイズ(この場合は1バイト)を考慮します。GoランタイムのCコードでは、メモリブロックの先頭アドレスや、型が不明な汎用的なメモリ領域を指すためによく使われていました。 -
uintptr(Go言語の符号なし整数型):uintptrは、ポインタのビットパターンを保持できるだけの大きさを持つ符号なし整数型です。これはGo言語の組み込み型であり、ポインタとは異なり、直接デリファレンスすることはできません。uintptrは、メモリ上のアドレスを数値として扱う場合に用いられます。例えば、アドレスの計算、アライメントの調整、あるいはGCが追跡すべきポインタではないことを明示する場合などに使用されます。uintptrは、Goのポインタ型 (*T) との間で明示的な型変換を行うことができますが、これは危険な操作であり、GCの追跡対象から外れる可能性があるため、ランタイムのような低レベルコードでのみ慎重に利用されます。
このコミットでは、byte* から uintptr への変更は、これらのフィールドが「メモリ上のアドレスを表す数値」であり、「GoのGCが追跡すべきオブジェクトへのポインタではない」ことを明確にする意図があります。これにより、GCはこれらの値をポインタとしてスキャンせず、純粋な数値として扱うことができます。
Goにおけるスタック管理
Goのゴルーチンは、それぞれ独立したスタックを持っています。Goのスタックは、必要に応じて動的にサイズが変更される「可変長スタック」です。関数呼び出しがスタックの現在の容量を超えそうになると、ランタイムはより大きな新しいスタックセグメントを割り当て、古いスタックの内容を新しいスタックにコピーします(morestack)。関数から戻る際にスタックが過剰に大きい場合は、より小さなスタックに縮小されることもあります(lessstack)。
スタックの境界(stackguard, stackbase)は、スタックオーバーフローを検出したり、GCがスタックをスキャンする範囲を決定したりするために重要です。これらの境界はメモリ上のアドレスで表現されるため、その型がGCの動作に影響を与えます。
G および Gobuf 構造体の役割
-
G構造体: GoランタイムにおけるG構造体は、個々のゴルーチンを表します。この構造体には、ゴルーチンの状態(実行中、待機中など)、スタックの境界 (stackguard,stackbase)、スケジューリング情報 (sched)、パニック情報 (panic) など、ゴルーチンに関するあらゆる重要な情報が格納されています。GCは、G構造体を通じて各ゴルーチンのスタックをスキャンし、到達可能なオブジェクトを特定します。 -
Gobuf構造体:Gobuf構造体は、ゴルーチンの実行コンテキスト(プログラムカウンタpc、スタックポインタsp、現在のゴルーチンg)を保存および復元するために使用されます。これは、ゴルーチンの切り替え(コンテキストスイッチ)や、システムコールへの出入り、スタックの拡張/縮小などの際に、ゴルーチンの実行状態を一時的に保存するために利用されます。
技術的詳細
このコミットの技術的な核心は、Goランタイムの内部データ構造におけるポインタ表現の厳密化と、それによるガベージコレクションの効率化および正確性の向上です。
GoのGCは、メモリ上の値をスキャンして、それがGoオブジェクトへのポインタであるかどうかを判断する必要があります。もし、単なる数値としてのアドレスを誤ってポインタとして解釈し、そのアドレスが不正なメモリ領域を指していた場合、GCはクラッシュしたり、誤ったメモリを解放したりする可能性があります。逆に、Goオブジェクトへの有効なポインタを数値として扱ってしまった場合、そのオブジェクトは到達不可能と判断され、誤って解放されてしまう(use-after-freeバグを引き起こす)可能性があります。
byte* から uintptr への型変更は、以下の目的を持っています。
- GCスキャンの最適化:
uintptr型のフィールドは、GCがポインタとして追跡する必要がないことを明示します。これにより、GCはこれらのフィールドをスキップし、スキャン対象を実際のGoオブジェクトポインタに限定できます。これは、GCのパフォーマンス向上に寄与します。 - メモリ安全性の向上: ランタイム内部でアドレスを数値として扱う場合、
uintptrを使用することで、コンパイラがポインタのデリファレンスを許可しないため、意図しないデリファレンスによるクラッシュを防ぐことができます。これは、ランタイムの堅牢性を高めます。 - セマンティクスの明確化:
uintptrを使用することで、そのフィールドが「メモリ上のアドレスを表す数値」であり、「Goの型システムが管理するオブジェクトへのポインタ」ではないというセマンティクスが明確になります。これにより、コードの可読性と保守性が向上します。
具体的に変更されたフィールドを見てみましょう。
-
Gobuf構造体内のspフィールド:sp(スタックポインタ) は、スタック上の特定の位置を指すアドレスです。GCはスタック全体をスキャンしますが、sp自体がGoオブジェクトへのポインタである必要はありません。uintptrにすることで、spが純粋なアドレス値として扱われ、GCがその値を追跡対象から外すことができます。 -
G構造体内のスタック関連フィールド (stackguard,stackbase,gcstack,gcsp,gcguard,stack0): これらのフィールドは、ゴルーチンのスタックの境界や、GC中に使用されるスタック関連のアドレスを保持します。これらはメモリ上の位置を示す数値であり、Goオブジェクトへのポインタではありません。uintptrに変更することで、GCがこれらの値をポインタとして誤って解釈することを防ぎ、スタックのスキャン範囲を正確に特定できるようになります。 -
Defer構造体内のargsフィールド:Defer構造体はdeferステートメントの情報を保持します。argsフィールドは、deferされた関数の引数を格納するための領域です。以前はbyte args[8]のように固定サイズのバイト配列でしたが、void* args[1]に変更され、さらにmallocsizの計算ロジックが追加されています。これは、引数のサイズが可変であること、そしてその領域がGCが直接追跡するGoオブジェクトへのポインタではなく、単なる生データ領域であることを示唆しています。void*はC言語における汎用ポインタであり、Goのランタイム内部で型安全性を犠牲にしてメモリを操作する際に用いられます。
これらの変更は、GoのGCがより洗練され、並行GCなどの高度な機能が導入されるための基盤を築くものでした。GCは、ポインタと非ポインタを正確に区別できる「ポインタマップ」のような情報を必要としますが、ランタイムの内部データ構造で uintptr を使用することは、この区別をより容易にする一歩となります。
コアとなるコードの変更箇所
このコミットの最も重要な変更は、src/pkg/runtime/runtime.h における構造体定義の変更です。
src/pkg/runtime/runtime.h
--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -167,7 +167,7 @@ struct Slice
struct Gobuf
{
// The offsets of these fields are known to (hard-coded in) libmach.
- byte* sp;
+ uintptr sp;
byte* pc;
G* g;
};
@@ -183,15 +183,15 @@ struct GCStats
};
struct G
{
- byte* stackguard; // cannot move - also known to linker, libmach, runtime/cgo
- byte* stackbase; // cannot move - also known to libmach, runtime/cgo
+ uintptr stackguard; // cannot move - also known to linker, libmach, runtime/cgo
+ uintptr stackbase; // cannot move - also known to libmach, runtime/cgo
Defer* defer;
Panic* panic;
Gobuf sched;
- byte* gcstack; // if status==Gsyscall, gcstack = stackbase to use during gc
- byte* gcsp; // if status==Gsyscall, gcsp = sched.sp to use during gc
- byte* gcguard; // if status==Gsyscall, gcguard = stackguard to use during gc
- byte* stack0;
+ uintptr gcstack; // if status==Gsyscall, gcstack = stackbase to use during gc
+ uintptr gcsp; // if status==Gsyscall, gcsp = sched.sp to use during gc
+ uintptr gcguard; // if status==Gsyscall, gcguard = stackguard to use during gc
+ uintptr stack0;
byte* entry; // initial function
G* alllink; // on allg
void* param; // passed parameter on wakeup
@@ -486,7 +486,7 @@ struct Defer
byte* pc;
byte* fn;
Defer* link;
- byte args[8]; // padded to actual size
+ void* args[1]; // padded to actual size
};
その他のファイルでの変更例
src/pkg/runtime/proc.c や src/pkg/runtime/mgc0.c など、ランタイムのCコードでは、上記 runtime.h で型が変更されたフィールドへのアクセス箇所で、明示的な型キャストが多数追加されています。
例: src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -318,7 +318,7 @@ runtime·tracebackothers(G *me)\
continue;\
runtime·printf("\n");\
runtime·goroutineheader(g);\
- runtime·traceback(g->sched.pc, g->sched.sp, 0, g);\
+ runtime·traceback(g->sched.pc, (byte*)g->sched.sp, 0, g);\
}\
}\
ここでは g->sched.sp が uintptr に変更されたため、runtime·traceback 関数に渡す際に (byte*) へキャストしています。これは、runtime·traceback が依然として byte* 型の引数を期待しているためです。
コアとなるコードの解説
runtime.h の変更
-
Gobuf.sp:Gobufはゴルーチンのコンテキストを保存する構造体です。spフィールドはスタックポインタを保持します。これをbyte*からuintptrに変更することで、spが純粋なメモリアドレスの数値表現として扱われるようになります。これにより、GCがこのフィールドをポインタとして追跡するのを防ぎ、GCの正確性とパフォーマンスを向上させます。 -
G構造体のスタック関連フィールド:stackguard,stackbase,gcstack,gcsp,gcguard,stack0はすべて、ゴルーチンのスタックの境界や、GC中に使用されるスタック関連のアドレスを定義するものです。これらをbyte*からuintptrに変更することは、これらのフィールドがGoオブジェクトへのポインタではなく、単なるメモリ上の位置を示す数値であることを明確にします。これにより、GCはこれらの値をスキャン対象から除外し、スタックの正確な範囲を数値として計算できるようになります。これは、GCがスタックをスキャンする際の効率と安全性を高める上で非常に重要です。 -
Defer.args:Defer構造体はdeferステートメントの情報を保持します。argsフィールドは、deferされた関数の引数を格納するための領域です。byte args[8]からvoid* args[1]への変更は、引数のサイズが固定ではなく、可変であることを示唆しています。void*はC言語における汎用ポインタであり、Goのランタイム内部で型安全性を犠牲にしてメモリを操作する際に用いられます。この変更は、deferの引数処理をより柔軟にするためのものであり、GCがこの領域を直接ポインタとして追跡するのではなく、必要に応じて内部的に処理することを意図しています。また、proc.cのruntime·deferproc関数でmallocsizの計算ロジックが追加されており、引数の実際のサイズに基づいてメモリを動的に確保するようになっています。
その他のファイルでのキャストの追加
runtime.h で型が変更されたことにより、cgocall.c, mgc0.c, mprof.goc, proc.c, traceback_arm.c, traceback_x86.c などのファイルでは、これらのフィールドを使用する際に明示的な型キャスト ((byte*) や (uintptr)) が追加されています。これは、C言語の型システムが厳密であるため、新しい uintptr 型の値を、以前 byte* を期待していた関数や操作に渡す際に必要となります。これらのキャストは、ランタイムの内部ロジックが引き続き正しく機能するようにするための適応であり、GCの変更に向けた準備の一環として行われています。
これらの変更は、Goのランタイムがメモリをより低レベルで、かつGCにとってより理解しやすい形で管理するための重要なステップでした。
関連リンク
- Go言語公式ドキュメント: https://golang.org/doc/
- Go言語のランタイムソースコード (GitHub): https://github.com/golang/go/tree/master/src/runtime
- Goのガベージコレクションに関するブログ記事やドキュメント(当時の状況を理解するために、古い情報源も参照すると良いでしょう)
参考にした情報源リンク
- Go言語の
uintptr型に関する公式ドキュメントや解説記事 - Go言語のガベージコレクションの歴史と進化に関する技術ブログや論文
- C言語におけるポインタと型変換に関する一般的な情報
- Goのランタイムソースコードの関連部分(特に
src/pkg/runtime/ディレクトリ) - GoのIssueトラッカーやメーリングリストでの関連議論(
golang.org/cl/6242061など) - Go 1.0 GCの特性に関するStack Overflowの議論: https://stackoverflow.com/questions/10467839/how-does-go-garbage-collection-work
uintptrとunsafe.Pointerの違いに関するGo言語のブログ記事: https://golangbridge.org/go-unsafe-pointer-vs-uintptr/- GoのGCの進化に関する記事: https://dev.to/vertexaisearch/go-garbage-collection-a-deep-dive-into-its-evolution-and-mechanisms-2024-update-411