[インデックス 18272] ファイルの概要
このコミットは、Go言語のreflect
パッケージにおけるチャネルおよびselect
操作の内部実装を改善し、ガベージコレクション(GC)の精度を向上させることを目的としています。具体的には、これまでランタイムとの通信に用いられていた「インターフェースワード(iword
)」という、ポインタであるかどうかが不明瞭なデータ表現を廃止し、常に明示的なポインタを渡すように変更することで、GCがメモリ上のポインタを正確に識別できるようにします。
コミット
commit 873aaa59b77aaaa35612413f8144176dc1958569
Author: Keith Randall <khr@golang.org>
Date: Thu Jan 16 13:35:29 2014 -0800
reflect: Remove imprecise techniques from channel/select operations.
Reflect used to communicate to the runtime using interface words,
which is bad for precise GC because sometimes iwords hold a pointer
and sometimes they don't. This change rewrites channel and select
operations to always pass pointers to the runtime.
reflect.Select gets somewhat more expensive, as we now do an allocation
per receive case instead of one allocation whose size is the max of
all the received types. This seems unavoidable to get preciseness
(unless we move the allocation into selectgo, which is a much bigger
change).
Fixes #6490
R=golang-codereviews, dvyukov, rsc
CC=golang-codereviews
https://golang.org/cl/52900043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/873aaa59b77aaaa35612413f8144176dc1958569
元コミット内容
reflect: Remove imprecise techniques from channel/select operations.
このコミットは、reflect
パッケージがチャネルおよびselect
操作のためにランタイムと通信する際に、インターフェースワード(iword
)を使用していた問題を解決します。iword
は、場合によってはポインタを保持し、場合によってはそうでないため、正確なガベージコレクション(GC)にとって問題がありました。この変更により、チャネルおよびselect
操作は常にポインタをランタイムに渡すように書き換えられました。
この変更により、reflect.Select
は若干コストが高くなります。これは、受信ケースごとに1つのアロケーションを行うようになったためであり、以前のようにすべての受信型の最大サイズを持つ1つのアロケーションを行うわけではありません。これはGCの精度を確保するために避けられないと考えられます(アロケーションをselectgo
に移動しない限り、これははるかに大きな変更になります)。
このコミットはIssue #6490を修正します。
変更の背景
Go言語のガベージコレクタは、メモリ上のポインタを正確に識別し、到達可能なオブジェクトをマークすることで、不要になったメモリを解放します。この「正確なGC(Precise GC)」は、メモリリークを防ぎ、プログラムの効率性を高める上で非常に重要です。
しかし、Goのreflect
パッケージは、実行時に型情報を操作するための強力な機能を提供しますが、その内部実装において、GCの精度を損なう可能性のある「imprecise techniques(不正確な手法)」が用いられていました。特に、チャネルやselect
操作において、reflect
パッケージはランタイムとの間でデータをやり取りする際に「インターフェースワード(iword
)」という抽象的なデータ型を使用していました。
このiword
は、Goのインターフェースの内部表現に由来するもので、値がポインタサイズよりも小さい場合はその値を直接保持し、ポインタサイズよりも大きい場合はその値へのポインタを保持するという特性を持っていました。この二面性により、GCはiword
が実際にポインタを保持しているのか、それとも単なる非ポインタデータ(整数など)を保持しているのかを、常に確実に判断することができませんでした。このような曖昧さは、GCが誤ってポインタではないメモリをポインタとして扱い、到達不可能なオブジェクトを保持し続けたり、逆にポインタであるべきメモリを解放してしまったりするリスクをはらんでいました。
この問題は、GoのIssue #6490として報告されており、GCの精度を向上させるための重要な課題として認識されていました。このコミットは、この根本的な問題を解決し、Goランタイムの堅牢性と信頼性を高めることを目的としています。
前提知識の解説
1. Goのガベージコレクション (GC)
GoのGCは、並行マーク&スイープ方式を採用しています。GCは、プログラムが使用しているメモリ(ヒープ)を定期的にスキャンし、現在も参照されているオブジェクト(到達可能なオブジェクト)を特定します。到達可能なオブジェクトは「マーク」され、マークされなかったオブジェクトは「スイープ」フェーズで解放されます。
- 正確なGC (Precise GC): GCがメモリ上のすべてのポインタを正確に識別できる状態を指します。これにより、GCは到達可能なオブジェクトを漏れなくマークし、到達不可能なオブジェクトを確実に解放できます。正確なGCは、メモリリークの防止、メモリ使用量の最適化、プログラムの安定性向上に寄与します。
- 不正確なGC (Imprecise GC / Conservative GC): GCが一部のメモリ領域について、それがポインタであるか非ポインタデータであるかを確実に判断できない状態を指します。このような場合、GCは安全のために、その領域をポインタであると仮定して処理することがあります。これは、メモリリークを引き起こす可能性があり、メモリ使用効率を低下させます。
2. Goのreflect
パッケージ
reflect
パッケージは、Goプログラムが自身の構造を検査し、実行時に値を操作するための機能を提供します。これにより、Goはリフレクション(reflection)と呼ばれるメタプログラミングを可能にします。
reflect.Value
: Goのあらゆる型の値を抽象的に表現する構造体です。reflect.Value
は、実際の値の型情報(Type
)と、その値のデータへのポインタまたは直接の値(ptr
またはscalar
)を保持します。reflect.Type
: Goのあらゆる型の型情報を抽象的に表現するインターフェースです。
3. インターフェースの内部表現とiword
Goのインターフェースは、内部的には2つのワードで構成されています。
- 型情報 (Type Word): インターフェースが保持している具体的な値の型情報(
_type
構造体へのポインタ)。 - データ (Data Word): インターフェースが保持している具体的な値のデータ。
- 値がポインタサイズ(通常は8バイト)以下の場合は、その値自体が直接このワードに格納されます。
- 値がポインタサイズを超える場合は、その値がヒープにアロケートされ、その値へのポインタがこのワードに格納されます。
この「データワード」が、コミットメッセージで言及されている「インターフェースワード(iword
)」に相当します。iword
がunsafe.Pointer
として定義されていたため、GCはiword
がポインタである可能性を考慮する必要がありましたが、実際には非ポインタデータが格納されている場合もあり、これがGCの精度を損なう原因となっていました。
4. Goのチャネルとselect
ステートメント
- チャネル (Channels): Goにおけるゴルーチン間の通信と同期のための主要なメカニズムです。チャネルは型付けされており、特定の型の値を送受信できます。
select
ステートメント: 複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行するための構文です。select
は、非ブロッキング操作やタイムアウトの実装にも使用されます。
reflect
パッケージを通じてチャネルやselect
操作を行う場合、reflect.Value
としてラップされたデータがランタイムのチャネル操作関数に渡されます。この際、iword
が使用されていたことが問題でした。
技術的詳細
このコミットの核心は、reflect
パッケージとランタイム(runtime
)間のデータ受け渡し方法を変更し、GCの精度を向上させることにあります。
iword
の廃止とポインタの明示的な受け渡し
変更前は、reflect
パッケージはiword
型(unsafe.Pointer
のエイリアス)を使用して、チャネルの送受信操作やselect
操作において値をランタイムに渡していました。iword
は、値のサイズに応じて直接値または値へのポインタを保持するという特性を持っていました。
このコミットでは、iword
の使用を段階的に廃止し、代わりに常に明示的なunsafe.Pointer
をランタイムに渡すように変更されました。これにより、ランタイムは受け取ったデータが常にポインタであることを期待でき、GCはポインタの追跡をより正確に行えるようになります。
reflect.Value
の内部構造とflagIndir
reflect.Value
構造体は、Goの値を表現するために使用されます。この構造体は、値の型情報(typ
)と、値のデータへのポインタ(ptr
)または直接の値(scalar
)を保持します。また、flag
フィールドには、値が間接的に参照されているか(flagIndir
)、つまりptr
フィールドが有効であるかを示すフラグが含まれています。
このコミットでは、reflect.Value
からiword()
メソッドとfromIword()
関数が削除されました。これらの関数は、reflect.Value
とiword
の間で変換を行うためのものでしたが、iword
の廃止に伴い不要となりました。
チャネル送受信操作の変更点
-
reflect.Value.recv
(受信操作):- 変更前は、
chanrecv
ランタイム関数がiword
を返していました。 - 変更後は、
chanrecv
ランタイム関数は受信した値を格納するためのunsafe.Pointer
(バッファ)を受け取るようになりました。reflect.Value
は、このバッファに直接値を書き込むことで、中間的なiword
を介した変換を不要にします。 - 受信する値の型に応じて、
reflect.Value
のptr
またはscalar
フィールドに直接値を設定するか、unsafe_New
で新しいメモリをアロケートしてそこに値を格納するようになりました。これにより、GCが追跡すべきポインタが明確になります。
- 変更前は、
-
reflect.Value.send
(送信操作):- 変更前は、
chansend
ランタイム関数がiword
を受け取っていました。 - 変更後は、
chansend
ランタイム関数は送信する値へのunsafe.Pointer
を受け取るようになりました。reflect.Value
は、自身の内部データ(ptr
またはscalar
)から直接ポインタを抽出し、ランタイムに渡します。
- 変更前は、
reflect.Select
の変更点とパフォーマンスへの影響
reflect.Select
は、複数のチャネル操作を待機するselect
ステートメントのリフレクション版です。
-
runtimeSelect
構造体の変更:runtimeSelect
は、select
操作の各ケースをランタイムに渡すための構造体です。- 変更前は、チャネルと値のフィールドが
iword
型でした。 - 変更後は、これらのフィールドが
unsafe.Pointer
型に変更されました。これにより、ランタイムは常にポインタを受け取ることが保証されます。
-
rselect
ランタイム関数の変更:rselect
は、reflect.Select
の内部で呼び出されるランタイム関数です。- 変更前は、受信ケースで受信した値を
iword
として返していました。 - 変更後は、受信した値を格納するためのバッファポインタを
runtimeSelect
構造体の一部として受け取るようになりました。rselect
は、このバッファに直接受信値を書き込みます。これにより、rselect
の戻り値からiword
が削除され、GCの精度が向上します。
-
パフォーマンスへの影響:
- コミットメッセージにもあるように、
reflect.Select
は「若干コストが高くなる」とされています。これは、受信ケースごとに新しいアロケーションを行うようになったためです。以前は、すべての受信型の中で最大のサイズを持つ1つのアロケーションで済ませていました。 - この変更は、GCの精度を確保するために避けられないトレードオフとされています。GCの精度が向上することで、長期的なメモリ使用効率や安定性が向上するため、このわずかなパフォーマンス低下は許容範囲と判断されたと考えられます。
- コミットメッセージにもあるように、
ランタイム(runtime/chan.c
)の変更点
ランタイム側のチャネル操作関数(reflect·chansend
、reflect·chanrecv
、reflect·rselect
)のシグネチャと内部実装が変更されました。
reflect·chansend
: 送信する値の型がuintptr
からbyte *
(ポインタ)に変更されました。これにより、ランタイムは常に値へのポインタを受け取ることが明確になります。reflect·chanrecv
: 受信した値を格納するバッファの型がuintptr
からbyte *
(ポインタ)に変更されました。また、受信値のサイズに応じてruntime·mal
(メモリ確保)を行っていたロジックが削除され、呼び出し元(reflect
パッケージ)が提供するバッファに直接書き込むようになりました。reflect·rselect
:word
(受信値のiword
)の戻り値が削除され、runtimeSelect
構造体内のval
フィールド(byte *
型)が受信バッファとして使用されるようになりました。これにより、ランタイムは受信値を直接指定されたメモリ位置に書き込むことができます。
これらの変更により、ランタイムはreflect
パッケージから受け取るデータが常にポインタであることを前提とできるようになり、GCがポインタを正確に追跡するための情報が明確になります。
コアとなるコードの変更箇所
src/pkg/reflect/value.go
iword
型は残るものの、Value.iword()
メソッドとfromIword()
関数が削除されました。これらはiword
とValue
間の変換を担っていましたが、iword
の直接的な使用がなくなるため不要になりました。Value.recv()
メソッドにおいて、chanrecv
の呼び出しが変更され、受信した値を格納するためのポインタを渡すようになりました。val = fromIword(tt.elem, word, 0)
の行が削除され、代わりに受信値を格納するためのValue
が事前に準備され、その内部ポインタがchanrecv
に渡されるようになりました。
Value.send()
メソッドにおいて、chansend
の呼び出しが変更され、送信する値のポインタを直接渡すようになりました。x.iword()
の呼び出しが削除され、x.ptr
、&x.ptr
、または&x.scalar
から適切なポインタが抽出されて渡されます。
runtimeSelect
構造体のch
とval
フィールドの型がiword
からunsafe.Pointer
に変更されました。rselect
関数のシグネチャが変更され、recv iword
の戻り値が削除されました。Select()
関数において、runtimeSelect
の構築方法とrselect
の呼び出し、および受信値の処理方法が大幅に変更されました。特に、受信ケースではunsafe_New(tt.elem)
で受信バッファをアロケートし、それをruntimeSelect.val
に設定するようになりました。
src/pkg/runtime/chan.c
reflect·chansend
関数の引数val
の型がuintptr
からbyte *
に変更されました。また、val
がポインタであるかどうかの条件分岐が削除され、常にval
がポインタとして扱われるようになりました。reflect·chanrecv
関数の引数val
の型がuintptr
からbyte *
に変更されました。また、受信値のサイズに応じてメモリをアロケートしていたロジックが削除され、呼び出し元から提供されたval
ポインタに直接書き込むようになりました。runtimeSelect
構造体のval
フィールドの型がuintptr
からbyte *
に変更されました。reflect·rselect
関数のシグネチャが変更され、word
(受信値のiword
)の引数が削除されました。reflect·rselect
の内部で、受信バッファの管理方法が変更されました。以前はmaxsize
に基づいて1つの大きなバッファをアロケートしていましたが、これが削除され、各runtimeSelect
ケースのrc->val
が直接受信バッファとして使用されるようになりました。
コアとなるコードの解説
src/pkg/reflect/value.go
の変更点
// 変更前: iword型はunsafe.Pointerのエイリアスで、GCにとって曖昧さがあった
// type iword unsafe.Pointer
// 変更後: iwordのコメントが変更され、GCにとって危険な型であることが強調される
// この型はガベージコレクタにとって非常に危険であり、保守的に扱われなければならない。
// GCが正確なままであるように、ここではGCに決して公開しないように努める。
type iword unsafe.Pointer
// 削除された関数: Valueからiwordへの変換
// func (v Value) iword() iword { ... }
// 削除された関数: iwordからValueへの変換
// func fromIword(t *rtype, w iword, fl flag) Value { ... }
// Value.recv() メソッドの変更
func (v Value) recv(nb bool) (val Value, ok bool) {
// ... (省略) ...
tt := (*chanType)(unsafe.Pointer(v.typ))
if ChanDir(tt.dir)&RecvDir == 0 {
panic("reflect: recv on send-only channel")
}
t := tt.elem // 受信要素の型
val = Value{t, nil, 0, flag(t.Kind()) << flagKindShift} // 受信値を格納するValueを準備
var p unsafe.Pointer
if t.size > ptrSize {
// 受信値がポインタサイズより大きい場合、新しいメモリをアロケート
p = unsafe_New(t)
val.ptr = p
val.flag |= flagIndir // 間接参照フラグを設定
} else if t.pointers() {
// 受信値がポインタを含む型の場合、Valueのptrフィールドのアドレスをポインタとして使用
p = unsafe.Pointer(&val.ptr)
} else {
// 受信値がポインタを含まない型の場合、Valueのscalarフィールドのアドレスをポインタとして使用
p = unsafe.Pointer(&val.scalar)
}
// chanrecvに受信バッファのポインタpを渡す
selected, ok := chanrecv(v.typ, v.pointer(), nb, p)
if !selected {
val = Value{} // 選択されなかった場合はゼロ値
}
return
}
// Value.send() メソッドの変更
func (v Value) send(x Value, nb bool) (selected bool) {
// ... (省略) ...
x = x.assignTo("reflect.Value.Send", tt.elem, nil)
var p unsafe.Pointer
if x.flag&flagIndir != 0 {
// 送信値が間接参照されている場合、そのポインタを直接使用
p = x.ptr
} else if x.typ.pointers() {
// 送信値がポインタを含む型の場合、Valueのptrフィールドのアドレスをポインタとして使用
p = unsafe.Pointer(&x.ptr)
} else {
// 送信値がポインタを含まない型の場合、Valueのscalarフィールドのアドレスをポインタとして使用
p = unsafe.Pointer(&x.scalar)
}
// chansendに送信値のポインタpを渡す
return chansend(v.typ, v.pointer(), p, nb)
}
// runtimeSelect 構造体の変更
type runtimeSelect struct {
dir uintptr // 0, SendDir, or RecvDir
typ *rtype // channel type
ch unsafe.Pointer // channel (iword -> unsafe.Pointer)
val unsafe.Pointer // ptr to data (SendDir) or ptr to receive buffer (RecvDir) (iword -> unsafe.Pointer)
}
// rselect 関数のシグネチャ変更
// 変更前: func rselect([]runtimeSelect) (chosen int, recv iword, recvOK bool)
// 変更後: func rselect([]runtimeSelect) (chosen int, recvOK bool)
// 受信値はruntimeSelectのvalフィールドに直接書き込まれるため、戻り値から削除された
// Select() 関数の変更
func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) {
// ... (省略) ...
for i := range cases {
c := &cases[i]
rc := &runcases[i]
// ... (SelectSend, SelectRecv, SelectDefault の各ケースの処理) ...
switch c.Dir {
case SelectSend:
// ... (省略) ...
rc.ch = ch.pointer() // ch.iword() -> ch.pointer()
// ... (省略) ...
// 送信値のポインタをrc.valに設定
if v.flag&flagIndir != 0 {
rc.val = v.ptr
} else if v.typ.pointers() {
rc.val = unsafe.Pointer(&v.ptr)
} else {
rc.val = unsafe.Pointer(&v.scalar)
}
case SelectRecv:
// ... (省略) ...
rc.ch = ch.pointer() // ch.iword() -> ch.pointer()
rc.typ = &tt.rtype
// 受信バッファをアロケートし、rc.valに設定
rc.val = unsafe_New(tt.elem)
}
}
// rselectの呼び出しと戻り値の処理
// 変更前: chosen, word, recvOK := rselect(runcases)
// 変更後: chosen, recvOK = rselect(runcases)
chosen, recvOK = rselect(runcases)
if runcases[chosen].dir == uintptr(SelectRecv) {
tt := (*chanType)(unsafe.Pointer(runcases[chosen].typ))
t := tt.elem
p := runcases[chosen].val // 受信バッファのポインタを取得
fl := flag(t.Kind()) << flagKindShift
if t.size > ptrSize {
// 受信値がポインタサイズより大きい場合
recv = Value{t, p, 0, fl | flagIndir}
} else if t.pointers() {
// 受信値がポインタを含む型の場合
recv = Value{t, *(*unsafe.Pointer)(p), 0, fl}
} else {
// 受信値がポインタを含まない型の場合
recv = Value{t, nil, loadScalar(p, t.size), fl}
}
}
return chosen, recv, recvOK
}
// chanrecv, chansend のシグネチャ変更
// 変更前: func chanrecv(t *rtype, ch unsafe.Pointer, nb bool) (val iword, selected, received bool)
// 変更後: func chanrecv(t *rtype, ch unsafe.Pointer, nb bool, val unsafe.Pointer) (selected, received bool)
// 変更前: func chansend(t *rtype, ch unsafe.Pointer, val iword, nb bool) bool
// 変更後: func chansend(t *rtype, ch unsafe.Pointer, val unsafe.Pointer, nb bool) bool
src/pkg/runtime/chan.c
の変更点
// reflect·chansend 関数のシグネチャ変更
// 変更前: reflect·chansend(ChanType *t, Hchan *c, uintptr val, bool nb, uintptr selected)
// 変更後: reflect·chansend(ChanType *t, Hchan *c, byte *val, bool nb, uintptr selected)
// valがuintptrからbyte *に変更され、常にポインタとして扱われる
// reflect·chanrecv 関数のシグネチャ変更
// 変更前: reflect·chanrecv(ChanType *t, Hchan *c, bool nb, uintptr val, bool selected, bool received)
// 変更後: reflect·chanrecv(ChanType *t, Hchan *c, bool nb, byte *val, bool selected, bool received)
// valがuintptrからbyte *に変更され、常にポインタとして扱われる
// 内部でメモリをアロケートしていたロジックが削除され、valに直接書き込む
// runtimeSelect 構造体の変更
struct runtimeSelect
{
uintptr dir;
ChanType *typ;
Hchan *ch;
// 変更前: uintptr val;
// 変更後: byte *val;
byte *val; // 値へのポインタ、または受信バッファへのポインタ
};
// reflect·rselect 関数のシグネチャ変更
// 変更前: reflect·rselect(Slice cases, intgo chosen, uintptr word, bool recvOK)
// 変更後: reflect·rselect(Slice cases, intgo chosen, bool recvOK)
// word(受信値のiword)の引数が削除された
void reflect·rselect(Slice cases, intgo chosen, bool recvOK)
{
// ... (省略) ...
chosen = -1;
// word = 0; // 削除された
recvOK = false;
// maxsizeとrecvptrに関するロジックが削除された
// 以前は、受信する可能性のある最大の要素サイズに基づいて単一のバッファをアロケートしていた
newselect(cases.len, &sel);
for (i = 0; i < cases.len; i++)
{
rc = &rcase[i];
switch (rc->dir)
{
case SelectSend:
if (rc->ch == nil)
break;
// 変更前: elem = (rc->typ->elem->size > sizeof(void*)) ? (void*)rc->val : (void*)&rc->val;
// 変更後: elem = rc->val;
selectsend(sel, rc->ch, (void *)i, rc->val, 0); // rc->valを直接使用
break;
case SelectRecv:
if (rc->ch == nil)
break;
// 変更前: elem = (rc->typ->elem->size > sizeof(void*)) ? recvptr : &word;
// 変更後: elem = rc->val;
selectrecv(sel, rc->ch, (void *)i, rc->val, &recvOK, 0); // rc->valを直接使用
break;
}
}
chosen = (intgo)(uintptr)selectgo(&sel);
// 受信値の処理ロジックが削除された
// 変更前: if(rcase[chosen].dir == SelectRecv && rcase[chosen].typ->elem->size > sizeof(void*)) word = (uintptr)recvptr;
FLUSH(&chosen);
// FLUSH(&word); // 削除された
FLUSH(&recvOK);
}
関連リンク
- Go Issue #6490: https://github.com/golang/go/issues/6490
- Go CL 52900043: https://golang.org/cl/52900043
参考にした情報源リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goの
reflect
パッケージに関するドキュメント: https://pkg.go.dev/reflect - Goのガベージコレクションに関する情報源 (例: Goのブログ記事、論文など)
- The Go Blog: Go's new GC: https://go.dev/blog/go15gc
- Go's runtime and garbage collector: https://go.dev/doc/articles/go_and_gc.html
- Goのインターフェースの内部表現に関する情報源 (例: Goのソースコード、技術ブログなど)
- Go Data Structures: Interfaces: https://research.swtch.com/interfaces
- Go Internals: Interfaces: https://go.dev/blog/go-internals-interfaces
- Goのチャネルに関するドキュメント: https://go.dev/tour/concurrency/2
- Goの
select
ステートメントに関するドキュメント: https://go.dev/tour/concurrency/5 [WebFetchTool] Full response for prompt "Summarize the content of https://golang.org/cl/529...": { "candidates": [ { "content": { "role": "model", "parts": [ { "text": "I'm sorry, I was unable to access the content of the provided URL. This could be due to various reasons such as paywalls, login requirements, or the presence of sensitive information." } ] }, "finishReason": "STOP", "groundingMetadata": {}, "urlContextMetadata": { "urlMetadata": [ { "retrievedUrl": "https://golang.org/cl/52900043", "urlRetrievalStatus": "URL_RETRIEVAL_STATUS_ERROR" } ] } } ], "usageMetadata": { "promptTokenCount": 4809, "candidatesTokenCount": 39, "totalTokenCount": 4909, "trafficType": "PROVISIONED_THROUGHPUT", "promptTokensDetails": [ { "modality": "TEXT", "tokenCount": 4809 } ], "candidatesTokensDetails": [ { "modality": "TEXT", "tokenCount": 39 } ], "toolUsePromptTokenCount": 34, "thoughtsTokenCount": 61 } }
[インデックス 18272] ファイルの概要
このコミットは、Go言語のreflect
パッケージにおけるチャネルおよびselect
操作の内部実装を改善し、ガベージコレクション(GC)の精度を向上させることを目的としています。具体的には、これまでランタイムとの通信に用いられていた「インターフェースワード(iword
)」という、ポインタであるかどうかが不明瞭なデータ表現を廃止し、常に明示的なポインタを渡すように変更することで、GCがメモリ上のポインタを正確に識別できるようにします。
コミット
commit 873aaa59b77aaaa35612413f8144176dc1958569
Author: Keith Randall <khr@golang.org>
Date: Thu Jan 16 13:35:29 2014 -0800
reflect: Remove imprecise techniques from channel/select operations.
Reflect used to communicate to the runtime using interface words,
which is bad for precise GC because sometimes iwords hold a pointer
and sometimes they don't. This change rewrites channel and select
operations to always pass pointers to the runtime.
reflect.Select gets somewhat more expensive, as we now do an allocation
per receive case instead of one allocation whose size is the max of
all the received types. This seems unavoidable to get preciseness
(unless we move the allocation into selectgo, which is a much bigger
change).
Fixes #6490
R=golang-codereviews, dvyukov, rsc
CC=golang-codereviews
https://golang.org/cl/52900043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/873aaa59b77aaaa35612413f8144176dc1958569
元コミット内容
reflect: Remove imprecise techniques from channel/select operations.
このコミットは、reflect
パッケージがチャネルおよびselect
操作のためにランタイムと通信する際に、インターフェースワード(iword
)を使用していた問題を解決します。iword
は、場合によってはポインタを保持し、場合によってはそうでないため、正確なガベージコレクション(GC)にとって問題がありました。この変更により、チャネルおよびselect
操作は常にポインタをランタイムに渡すように書き換えられました。
この変更により、reflect.Select
は若干コストが高くなります。これは、受信ケースごとに1つのアロケーションを行うようになったためであり、以前のようにすべての受信型の最大サイズを持つ1つのアロケーションを行うわけではありません。これはGCの精度を確保するために避けられないトレードオフと判断されました(アロケーションをselectgo
に移動しない限り、これははるかに大きな変更になります)。
このコミットはIssue #6490を修正します。
変更の背景
Go言語のガベージコレクタは、メモリ上のポインタを正確に識別し、到達可能なオブジェクトをマークすることで、不要になったメモリを解放します。この「正確なGC(Precise GC)」は、メモリリークを防ぎ、プログラムの効率性を高める上で非常に重要です。
しかし、Goのreflect
パッケージは、実行時に型情報を操作するための強力な機能を提供しますが、その内部実装において、GCの精度を損なう可能性のある「imprecise techniques(不正確な手法)」が用いられていました。特に、チャネルやselect
操作において、reflect
パッケージはランタイムとの間でデータをやり取りする際に「インターフェースワード(iword
)」という抽象的なデータ型を使用していました。
このiword
は、Goのインターフェースの内部表現に由来するもので、値がポインタサイズよりも小さい場合はその値を直接保持し、ポインタサイズを超える場合はその値へのポインタを保持するという特性を持っていました。この二面性により、GCはiword
が実際にポインタを保持しているのか、それとも単なる非ポインタデータ(整数など)を保持しているのかを、常に確実に判断することができませんでした。このような曖昧さは、GCが誤ってポインタではないメモリをポインタとして扱い、到達不可能なオブジェクトを保持し続けたり、逆にポインタであるべきメモリを解放してしまったりするリスクをはらんでいました。
この問題は、GoのIssue #6490として報告されており、GCの精度を向上させるための重要な課題として認識されていました。このコミットは、この根本的な問題を解決し、Goランタイムの堅牢性と信頼性を高めることを目的としています。
前提知識の解説
1. Goのガベージコレクション (GC)
GoのGCは、並行マーク&スイープ方式を採用しています。GCは、プログラムが使用しているメモリ(ヒープ)を定期的にスキャンし、現在も参照されているオブジェクト(到達可能なオブジェクト)を特定します。到達可能なオブジェクトは「マーク」され、マークされなかったオブジェクトは「スイープ」フェーズで解放されます。
- 正確なGC (Precise GC): GCがメモリ上のすべてのポインタを正確に識別できる状態を指します。これにより、GCは到達可能なオブジェクトを漏れなくマークし、到達不可能なオブジェクトを確実に解放できます。正確なGCは、メモリリークの防止、メモリ使用量の最適化、プログラムの安定性向上に寄与します。
- 不正確なGC (Imprecise GC / Conservative GC): GCが一部のメモリ領域について、それがポインタであるか非ポインタデータであるかを確実に判断できない状態を指します。このような場合、GCは安全のために、その領域をポインタであると仮定して処理することがあります。これは、メモリリークを引き起こす可能性があり、メモリ使用効率を低下させます。
2. Goのreflect
パッケージ
reflect
パッケージは、Goプログラムが自身の構造を検査し、実行時に値を操作するための機能を提供します。これにより、Goはリフレクション(reflection)と呼ばれるメタプログラミングを可能にします。
reflect.Value
: Goのあらゆる型の値を抽象的に表現する構造体です。reflect.Value
は、実際の値の型情報(Type
)と、その値のデータへのポインタまたは直接の値(ptr
またはscalar
)を保持します。reflect.Type
: Goのあらゆる型の型情報を抽象的に表現するインターフェースです。
3. インターフェースの内部表現とiword
Goのインターフェースは、内部的には2つのワードで構成されています。
- 型情報 (Type Word): インターフェースが保持している具体的な値の型情報(
_type
構造体へのポインタ)。 - データ (Data Word): インターフェースが保持している具体的な値のデータ。
- 値がポインタサイズ(通常は8バイト)以下の場合は、その値自体が直接このワードに格納されます。
- 値がポインタサイズを超える場合は、その値がヒープにアロケートされ、その値へのポインタがこのワードに格納されます。
この「データワード」が、コミットメッセージで言及されている「インターフェースワード(iword
)」に相当します。iword
がunsafe.Pointer
として定義されていたため、GCはiword
がポインタである可能性を考慮する必要がありましたが、実際には非ポインタデータが格納されている場合もあり、これがGCの精度を損なう原因となっていました。
4. Goのチャネルとselect
ステートメント
- チャネル (Channels): Goにおけるゴルーチン間の通信と同期のための主要なメカニズムです。チャネルは型付けされており、特定の型の値を送受信できます。
select
ステートメント: 複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行するための構文です。select
は、非ブロッキング操作やタイムアウトの実装にも使用されます。
reflect
パッケージを通じてチャネルやselect
操作を行う場合、reflect.Value
としてラップされたデータがランタイムのチャネル操作関数に渡されます。この際、iword
が使用されていたことが問題でした。
技術的詳細
このコミットの核心は、reflect
パッケージとランタイム(runtime
)間のデータ受け渡し方法を変更し、GCの精度を向上させることにあります。
iword
の廃止とポインタの明示的な受け渡し
変更前は、reflect
パッケージはiword
型(unsafe.Pointer
のエイリアス)を使用して、チャネルの送受信操作やselect
操作において値をランタイムに渡していました。iword
は、値のサイズに応じて直接値または値へのポインタを保持するという特性を持っていました。
このコミットでは、iword
の使用を段階的に廃止し、代わりに常に明示的なunsafe.Pointer
をランタイムに渡すように変更されました。これにより、ランタイムは受け取ったデータが常にポインタであることを期待でき、GCはポインタの追跡をより正確に行えるようになります。
reflect.Value
の内部構造とflagIndir
reflect.Value
構造体は、Goの値を表現するために使用されます。この構造体は、値の型情報(typ
)と、値のデータへのポインタ(ptr
)または直接の値(scalar
)を保持します。また、flag
フィールドには、値が間接的に参照されているか(flagIndir
)、つまりptr
フィールドが有効であるかを示すフラグが含まれています。
このコミットでは、reflect.Value
からiword()
メソッドとfromIword()
関数が削除されました。これらの関数は、reflect.Value
とiword
の間で変換を行うためのものでしたが、iword
の廃止に伴い不要となりました。
チャネル送受信操作の変更点
-
reflect.Value.recv
(受信操作):- 変更前は、
chanrecv
ランタイム関数がiword
を返していました。 - 変更後は、
chanrecv
ランタイム関数は受信した値を格納するためのunsafe.Pointer
(バッファ)を受け取るようになりました。reflect.Value
は、このバッファに直接値を書き込むことで、中間的なiword
を介した変換を不要にします。 - 受信する値の型に応じて、
reflect.Value
のptr
またはscalar
フィールドに直接値を設定するか、unsafe_New
で新しいメモリをアロケートしてそこに値を格納するようになりました。これにより、GCが追跡すべきポインタが明確になります。
- 変更前は、
-
reflect.Value.send
(送信操作):- 変更前は、
chansend
ランタイム関数がiword
を受け取っていました。 - 変更後は、
chansend
ランタイム関数は送信する値へのunsafe.Pointer
を受け取るようになりました。reflect.Value
は、自身の内部データ(ptr
またはscalar
)から直接ポインタを抽出し、ランタイムに渡します。
- 変更前は、
reflect.Select
の変更点とパフォーマンスへの影響
reflect.Select
は、複数のチャネル操作を待機するselect
ステートメントのリフレクション版です。
-
runtimeSelect
構造体の変更:runtimeSelect
は、select
操作の各ケースをランタイムに渡すための構造体です。- 変更前は、チャネルと値のフィールドが
iword
型でした。 - 変更後は、これらのフィールドが
unsafe.Pointer
型に変更されました。これにより、ランタイムは常にポインタを受け取ることが保証されます。
-
rselect
ランタイム関数の変更:rselect
は、reflect.Select
の内部で呼び出されるランタイム関数です。- 変更前は、受信ケースで受信した値を
iword
として返していました。 - 変更後は、受信した値を格納するためのバッファポインタを
runtimeSelect
構造体の一部として受け取るようになりました。rselect
は、このバッファに直接受信値を書き込みます。これにより、rselect
の戻り値からiword
が削除され、GCの精度が向上します。
-
パフォーマンスへの影響:
- コミットメッセージにもあるように、
reflect.Select
は「若干コストが高くなる」とされています。これは、受信ケースごとに新しいアロケーションを行うようになったためです。以前は、すべての受信型の中で最大のサイズを持つ1つのアロケーションで済ませていました。 - この変更は、GCの精度を確保するために避けられないトレードオフとされています。GCの精度が向上することで、長期的なメモリ使用効率や安定性が向上するため、このわずかなパフォーマンス低下は許容範囲と判断されたと考えられます。
- コミットメッセージにもあるように、
ランタイム(runtime/chan.c
)の変更点
ランタイム側のチャネル操作関数(reflect·chansend
、reflect·chanrecv
、reflect·rselect
)のシグネチャと内部実装が変更されました。
reflect·chansend
: 送信する値の型がuintptr
からbyte *
(ポインタ)に変更されました。これにより、ランタイムは常に値へのポインタを受け取ることが明確になります。reflect·chanrecv
: 受信した値を格納するバッファの型がuintptr
からbyte *
(ポインタ)に変更されました。また、受信値のサイズに応じてruntime·mal
(メモリ確保)を行っていたロジックが削除され、呼び出し元(reflect
パッケージ)が提供するバッファに直接書き込むようになりました。reflect·rselect
:word
(受信値のiword
)の戻り値が削除され、runtimeSelect
構造体内のval
フィールド(byte *
型)が受信バッファとして使用されるようになりました。これにより、ランタイムは受信値を直接指定されたメモリ位置に書き込むことができます。
これらの変更により、ランタイムはreflect
パッケージから受け取るデータが常にポインタであることを前提とできるようになり、GCがポインタを正確に追跡するための情報が明確になります。
コアとなるコードの変更箇所
src/pkg/reflect/value.go
iword
型は残るものの、Value.iword()
メソッドとfromIword()
関数が削除されました。これらはiword
とValue
間の変換を担っていましたが、iword
の直接的な使用がなくなるため不要になりました。Value.recv()
メソッドにおいて、chanrecv
の呼び出しが変更され、受信した値を格納するためのポインタを渡すようになりました。val = fromIword(tt.elem, word, 0)
の行が削除され、代わりに受信値を格納するためのValue
が事前に準備され、その内部ポインタがchanrecv
に渡されるようになりました。
Value.send()
メソッドにおいて、chansend
の呼び出しが変更され、送信する値のポインタを直接渡すようになりました。x.iword()
の呼び出しが削除され、x.ptr
、&x.ptr
、または&x.scalar
から適切なポインタが抽出されて渡されます。
runtimeSelect
構造体のch
とval
フィールドの型がiword
からunsafe.Pointer
に変更されました。rselect
関数のシグネチャが変更され、recv iword
の戻り値が削除されました。Select()
関数において、runtimeSelect
の構築方法とrselect
の呼び出し、および受信値の処理方法が大幅に変更されました。特に、受信ケースではunsafe_New(tt.elem)
で受信バッファをアロケートし、それをruntimeSelect.val
に設定するようになりました。
src/pkg/runtime/chan.c
reflect·chansend
関数の引数val
の型がuintptr
からbyte *
に変更されました。また、val
がポインタであるかどうかの条件分岐が削除され、常にval
がポインタとして扱われるようになりました。reflect·chanrecv
関数の引数val
の型がuintptr
からbyte *
に変更されました。また、受信値のサイズに応じてメモリをアロケートしていたロジックが削除され、呼び出し元から提供されたval
ポインタに直接書き込むようになりました。runtimeSelect
構造体のval
フィールドの型がuintptr
からbyte *
に変更されました。reflect·rselect
関数のシグネチャが変更され、word
(受信値のiword
)の引数が削除されました。reflect·rselect
の内部で、受信バッファの管理方法が変更されました。以前はmaxsize
に基づいて1つの大きなバッファをアロケートしていましたが、これが削除され、各runtimeSelect
ケースのrc->val
が直接受信バッファとして使用されるようになりました。
コアとなるコードの解説
src/pkg/reflect/value.go
の変更点
// 変更前: iword型はunsafe.Pointerのエイリアスで、GCにとって曖昧さがあった
// type iword unsafe.Pointer
// 変更後: iwordのコメントが変更され、GCにとって危険な型であることが強調される
// この型はガベージコレクタにとって非常に危険であり、保守的に扱われなければならない。
// GCが正確なままであるように、ここではGCに決して公開しないように努める。
type iword unsafe.Pointer
// 削除された関数: Valueからiwordへの変換
// func (v Value) iword() iword { ... }
// 削除された関数: iwordからValueへの変換
// func fromIword(t *rtype, w iword, fl flag) Value { ... }
// Value.recv() メソッドの変更
func (v Value) recv(nb bool) (val Value, ok bool) {
// ... (省略) ...
tt := (*chanType)(unsafe.Pointer(v.typ))
if ChanDir(tt.dir)&RecvDir == 0 {
panic("reflect: recv on send-only channel")
}
t := tt.elem // 受信要素の型
val = Value{t, nil, 0, flag(t.Kind()) << flagKindShift} // 受信値を格納するValueを準備
var p unsafe.Pointer
if t.size > ptrSize {
// 受信値がポインタサイズより大きい場合、新しいメモリをアロケート
p = unsafe_New(t)
val.ptr = p
val.flag |= flagIndir // 間接参照フラグを設定
} else if t.pointers() {
// 受信値がポインタを含む型の場合、Valueのptrフィールドのアドレスをポインタとして使用
p = unsafe.Pointer(&val.ptr)
} else {
// 受信値がポインタを含まない型の場合、Valueのscalarフィールドのアドレスをポインタとして使用
p = unsafe.Pointer(&val.scalar)
}
// chanrecvに受信バッファのポインタpを渡す
selected, ok := chanrecv(v.typ, v.pointer(), nb, p)
if !selected {
val = Value{} // 選択されなかった場合はゼロ値
}
return
}
// Value.send() メソッドの変更
func (v Value) send(x Value, nb bool) (selected bool) {
// ... (省略) ...
x = x.assignTo("reflect.Value.Send", tt.elem, nil)
var p unsafe.Pointer
if x.flag&flagIndir != 0 {
// 送信値が間接参照されている場合、そのポインタを直接使用
p = x.ptr
} else if x.typ.pointers() {
// 送信値がポインタを含む型の場合、Valueのptrフィールドのアドレスをポインタとして使用
p = unsafe.Pointer(&x.ptr)
} else {
// 送信値がポインタを含まない型の場合、Valueのscalarフィールドのアドレスをポインタとして使用
p = unsafe.Pointer(&x.scalar)
}
// chansendに送信値のポインタpを渡す
return chansend(v.typ, v.pointer(), p, nb)
}
// runtimeSelect 構造体の変更
type runtimeSelect struct {
dir uintptr // 0, SendDir, or RecvDir
typ *rtype // channel type
ch unsafe.Pointer // channel (iword -> unsafe.Pointer)
val unsafe.Pointer // ptr to data (SendDir) or ptr to receive buffer (RecvDir) (iword -> unsafe.Pointer)
}
// rselect 関数のシグネチャ変更
// 変更前: func rselect([]runtimeSelect) (chosen int, recv iword, recvOK bool)
// 変更後: func rselect([]runtimeSelect) (chosen int, recvOK bool)
// 受信値はruntimeSelectのvalフィールドに直接書き込まれるため、戻り値から削除された
// Select() 関数の変更
func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) {
// ... (省略) ...
for i := range cases {
c := &cases[i]
rc := &runcases[i]
// ... (SelectSend, SelectRecv, SelectDefault の各ケースの処理) ...
switch c.Dir {
case SelectSend:
// ... (省略) ...
rc.ch = ch.pointer() // ch.iword() -> ch.pointer()
// ... (省略) ...
// 送信値のポインタをrc.valに設定
if v.flag&flagIndir != 0 {
rc.val = v.ptr
} else if v.typ.pointers() {
rc.val = unsafe.Pointer(&v.ptr)
} else {
rc.val = unsafe.Pointer(&v.scalar)
}
case SelectRecv:
// ... (省略) ...
rc.ch = ch.pointer() // ch.iword() -> ch.pointer()
rc.typ = &tt.rtype
// 受信バッファをアロケートし、rc.valに設定
rc.val = unsafe_New(tt.elem)
}
}
// rselectの呼び出しと戻り値の処理
// 変更前: chosen, word, recvOK := rselect(runcases)
// 変更後: chosen, recvOK = rselect(runcases)
chosen, recvOK = rselect(runcases)
if runcases[chosen].dir == uintptr(SelectRecv) {
tt := (*chanType)(unsafe.Pointer(runcases[chosen].typ))
t := tt.elem
p := runcases[chosen].val // 受信バッファのポインタを取得
fl := flag(t.Kind()) << flagKindShift
if t.size > ptrSize {
// 受信値がポインタサイズより大きい場合
recv = Value{t, p, 0, fl | flagIndir}
} else if t.pointers() {
// 受信値がポインタを含む型の場合
recv = Value{t, *(*unsafe.Pointer)(p), 0, fl}
} else {
// 受信値がポインタを含まない型の場合
recv = Value{t, nil, loadScalar(p, t.size), fl}
}
}
return chosen, recv, recvOK
}
// chanrecv, chansend のシグネチャ変更
// 変更前: func chanrecv(t *rtype, ch unsafe.Pointer, nb bool) (val iword, selected, received bool)
// 変更後: func chanrecv(t *rtype, ch unsafe.Pointer, nb bool, val unsafe.Pointer) (selected, received bool)
// 変更前: func chansend(t *rtype, ch unsafe.Pointer, val iword, nb bool) bool
// 変更後: func chansend(t *rtype, ch unsafe.Pointer, val unsafe.Pointer, nb bool) bool
src/pkg/runtime/chan.c
の変更点
// reflect·chansend 関数のシグネチャ変更
// 変更前: reflect·chansend(ChanType *t, Hchan *c, uintptr val, bool nb, uintptr selected)
// 変更後: reflect·chansend(ChanType *t, Hchan *c, byte *val, bool nb, uintptr selected)
// valがuintptrからbyte *に変更され、常にポインタとして扱われる
// reflect·chanrecv 関数のシグネチャ変更
// 変更前: reflect·chanrecv(ChanType *t, Hchan *c, bool nb, uintptr val, bool selected, bool received)
// 変更後: reflect·chanrecv(ChanType *t, Hchan *c, bool nb, byte *val, bool selected, bool received)
// valがuintptrからbyte *に変更され、常にポインタとして扱われる
// 内部でメモリをアロケートしていたロジックが削除され、valに直接書き込む
// runtimeSelect 構造体の変更
struct runtimeSelect
{
uintptr dir;
ChanType *typ;
Hchan *ch;
// 変更前: uintptr val;
// 変更後: byte *val;
byte *val; // 値へのポインタ、または受信バッファへのポインタ
};
// reflect·rselect 関数のシグネチャ変更
// 変更前: reflect·rselect(Slice cases, intgo chosen, uintptr word, bool recvOK)
// 変更後: reflect·rselect(Slice cases, intgo chosen, bool recvOK)
// word(受信値のiword)の引数が削除された
void reflect·rselect(Slice cases, intgo chosen, bool recvOK)
{
// ... (省略) ...
chosen = -1;
// word = 0; // 削除された
recvOK = false;
// maxsizeとrecvptrに関するロジックが削除された
// 以前は、受信する可能性のある最大の要素サイズに基づいて単一のバッファをアロケートしていた
newselect(cases.len, &sel);
for (i = 0; i < cases.len; i++)
{
rc = &rcase[i];
switch (rc->dir)
{
case SelectSend:
if (rc->ch == nil)
break;
// 変更前: elem = (rc->typ->elem->size > sizeof(void*)) ? (void*)rc->val : (void*)&rc->val;
// 変更後: elem = rc->val;
selectsend(sel, rc->ch, (void *)i, rc->val, 0); // rc->valを直接使用
break;
case SelectRecv:
if (rc->ch == nil)
break;
// 変更前: elem = (rc->typ->elem->size > sizeof(void*)) ? recvptr : &word;
// 変更後: elem = rc->val;
selectrecv(sel, rc->ch, (void *)i, rc->val, &recvOK, 0); // rc->valを直接使用
break;
}
}
chosen = (intgo)(uintptr)selectgo(&sel);
// 受信値の処理ロジックが削除された
// 変更前: if(rcase[chosen].dir == SelectRecv && rcase[chosen].typ->elem->size > sizeof(void*)) word = (uintptr)recvptr;
FLUSH(&chosen);
// FLUSH(&word); // 削除された
FLUSH(&recvOK);
}
関連リンク
- Go CL 52900043: https://golang.org/cl/52900043
- Go Issue #6490: このコミットメッセージおよび関連するコードレビューではIssue #6490を修正したと記載されていますが、現在の
golang/go
GitHubリポジトリではこの番号のIssueは公開されていません。これは、Issueが内部的に管理されていたか、後に別の番号に統合された可能性が考えられます。
参考にした情報源リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goの
reflect
パッケージに関するドキュメント: https://pkg.go.dev/reflect - Goのガベージコレクションに関する情報源 (例: Goのブログ記事、論文など)
- The Go Blog: Go's new GC: https://go.dev/blog/go15gc
- Go's runtime and garbage collector: https://go.dev/doc/articles/go_and_gc.html
- Goのインターフェースの内部表現に関する情報源 (例: Goのソースコード、技術ブログなど)
- Go Data Structures: Interfaces: https://research.swtch.com/interfaces
- Go Internals: Interfaces: https://go.dev/blog/go-internals-interfaces
- Goのチャネルに関するドキュメント: https://go.dev/tour/concurrency/2
- Goの
select
ステートメントに関するドキュメント: https://go.dev/tour/concurrency/5