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

[インデックス 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)」に相当します。iwordunsafe.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.Valueiwordの間で変換を行うためのものでしたが、iwordの廃止に伴い不要となりました。

チャネル送受信操作の変更点

  • reflect.Value.recv (受信操作):

    • 変更前は、chanrecvランタイム関数がiwordを返していました。
    • 変更後は、chanrecvランタイム関数は受信した値を格納するためのunsafe.Pointer(バッファ)を受け取るようになりました。reflect.Valueは、このバッファに直接値を書き込むことで、中間的なiwordを介した変換を不要にします。
    • 受信する値の型に応じて、reflect.Valueptrまたは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·chansendreflect·chanrecvreflect·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()関数が削除されました。これらはiwordValue間の変換を担っていましたが、iwordの直接的な使用がなくなるため不要になりました。
  • Value.recv()メソッドにおいて、chanrecvの呼び出しが変更され、受信した値を格納するためのポインタを渡すようになりました。
    • val = fromIword(tt.elem, word, 0) の行が削除され、代わりに受信値を格納するためのValueが事前に準備され、その内部ポインタがchanrecvに渡されるようになりました。
  • Value.send()メソッドにおいて、chansendの呼び出しが変更され、送信する値のポインタを直接渡すようになりました。
    • x.iword() の呼び出しが削除され、x.ptr&x.ptr、または&x.scalarから適切なポインタが抽出されて渡されます。
  • runtimeSelect構造体のchvalフィールドの型が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言語の公式ドキュメント: https://golang.org/doc/
  • Goのreflectパッケージに関するドキュメント: https://pkg.go.dev/reflect
  • Goのガベージコレクションに関する情報源 (例: Goのブログ記事、論文など)
  • Goのインターフェースの内部表現に関する情報源 (例: Goのソースコード、技術ブログなど)
  • 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)」に相当します。iwordunsafe.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.Valueiwordの間で変換を行うためのものでしたが、iwordの廃止に伴い不要となりました。

チャネル送受信操作の変更点

  • reflect.Value.recv (受信操作):

    • 変更前は、chanrecvランタイム関数がiwordを返していました。
    • 変更後は、chanrecvランタイム関数は受信した値を格納するためのunsafe.Pointer(バッファ)を受け取るようになりました。reflect.Valueは、このバッファに直接値を書き込むことで、中間的なiwordを介した変換を不要にします。
    • 受信する値の型に応じて、reflect.Valueptrまたは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·chansendreflect·chanrecvreflect·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()関数が削除されました。これらはiwordValue間の変換を担っていましたが、iwordの直接的な使用がなくなるため不要になりました。
  • Value.recv()メソッドにおいて、chanrecvの呼び出しが変更され、受信した値を格納するためのポインタを渡すようになりました。
    • val = fromIword(tt.elem, word, 0) の行が削除され、代わりに受信値を格納するためのValueが事前に準備され、その内部ポインタがchanrecvに渡されるようになりました。
  • Value.send()メソッドにおいて、chansendの呼び出しが変更され、送信する値のポインタを直接渡すようになりました。
    • x.iword() の呼び出しが削除され、x.ptr&x.ptr、または&x.scalarから適切なポインタが抽出されて渡されます。
  • runtimeSelect構造体のchvalフィールドの型が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が内部的に管理されていたか、後に別の番号に統合された可能性が考えられます。

参考にした情報源リンク