[インデックス 18287] ファイルの概要
このコミットでは、Goランタイムとコンパイラ(cmd/gc
)におけるチャネル操作の内部実装が変更されています。具体的には、チャネルへの値の送受信やselect
文での非ブロック操作に関連するランタイム関数が、可変引数(vararg)C関数から、明示的にポインタを渡す形式に変更されました。これにより、ガベージコレクタ(GC)がスタック上のポインタを正確に識別できるようになり、GCの安全性が向上します。
変更されたファイルは以下の通りです。
src/cmd/gc/builtin.c
src/cmd/gc/runtime.go
src/cmd/gc/select.c
src/cmd/gc/walk.c
src/pkg/runtime/chan.c
コミット
commit 6f6a9445c93bfbfd05ea9b7880137c02618bedbd
Author: Keith Randall <khr@golang.org>
Date: Fri Jan 17 14:48:45 2014 -0800
runtime, cmd/gc: Get rid of vararg channel calls.
Vararg C calls present a problem for the GC because the
argument types are not derivable from the signature. Remove
them by passing pointers to channel elements instead of the
channel elements directly.
R=golang-codereviews, gobot, rsc, dvyukov
CC=golang-codereviews
https://golang.org/cl/53430043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6f6a9445c93bfbfd05ea9b7880137c02618bedbd
元コミット内容
Goランタイムとコンパイラ(cmd/gc
)において、可変引数(vararg)チャネル呼び出しを廃止する変更です。可変引数C関数は、そのシグネチャから引数の型を導出できないため、ガベージコレクタにとって問題となります。この問題を解決するため、チャネル要素を直接渡す代わりに、チャネル要素へのポインタを渡すように変更されました。
変更の背景
この変更の主な背景は、Go言語のガベージコレクタ(GC)の正確性と安全性にあります。GoのGCは、ヒープ上に割り当てられたオブジェクトのうち、到達可能なもの(プログラムから参照され続けているもの)を特定するために、スタックやレジスタをスキャンしてポインタを探します。
C言語の可変引数関数(...
を使用する関数)は、コンパイル時に引数の型が固定されず、実行時に渡される引数の数や型が変化する可能性があります。GoのランタイムはC言語で実装された部分が多く、チャネル操作のような低レベルな処理の一部で可変引数C関数が使用されていました。
問題は、GCがスタックをスキャンする際に、可変引数関数のスタックフレームに遭遇した場合に発生します。GCはスタック上のどのメモリ領域がポインタであり、どのメモリ領域が単なる値であるかを正確に知る必要があります。しかし、可変引数C関数では、そのシグネチャだけでは引数の型情報が不明瞭なため、GCがスタック上のポインタを確実に識別することができませんでした。これにより、GCが誤ってポインタではないメモリをポインタと解釈したり、逆にポインタを見落としたりするリスクがあり、メモリリークやクラッシュといった深刻なバグにつながる可能性がありました。
このコミットは、このGCの課題を解決するために、チャネル関連のランタイム関数から可変引数を排除し、代わりに明示的なポインタ引数を使用することで、GCがスタックを正確にスキャンできるようにすることを目的としています。
前提知識の解説
Goのガベージコレクション (GC)
GoのGCは、並行マーク&スイープ方式を採用しています。GCの主要なタスクの一つは、プログラムの実行中にヒープ上に割り当てられたメモリのうち、もはや到達不可能になったオブジェクトを特定し、そのメモリを解放することです。このプロセスにおいて、GCは「ルート」(グローバル変数、スタック上の変数、レジスタなど)から開始し、それらが参照するオブジェクトをマークしていきます。この「マーク」フェーズでは、スタック上の変数がポインタであるかどうかを正確に判断し、それが指すヒープ上のオブジェクトを追跡する必要があります。もしスタック上のポインタを誤って識別すると、GCは到達可能なオブジェクトを解放してしまったり(use-after-free)、到達不可能なオブジェクトを解放し損ねたり(メモリリーク)する可能性があります。
C言語の可変引数 (Varargs)
C言語では、stdarg.h
ヘッダの va_list
, va_start
, va_arg
, va_end
マクロを使用して可変引数関数を定義できます。関数のプロトタイプでは、固定引数の後に ...
を記述します。
例: int printf(const char *format, ...);
可変引数関数が呼び出されると、引数は通常、スタックに積まれます。しかし、呼び出し側は引数の型情報を関数に明示的に渡しません。関数内部では、va_arg
マクロを使って引数を順に取り出しますが、この際に引数の型をプログラマが指定する必要があります。この仕組みのため、コンパイラは可変引数関数の呼び出しサイトで、個々の引数の正確な型情報を静的に把握することが困難です。これが、GCがスタックをスキャンする際に問題となる根本原因です。
Goのチャネルの内部実装
Goのチャネルは、ゴルーチン間の安全な通信手段を提供します。その実装は、Goランタイム(src/pkg/runtime
)内のCコード(特にchan.c
)と、Goコンパイラ(src/cmd/gc
)が生成するコードによって行われます。Goのチャネル操作(send
, receive
, close
, select
)は、コンパイラによって対応するランタイム関数呼び出しに変換されます。これらのランタイム関数は、チャネルのバッファ管理、ゴルーチンのスケジューリング、デッドロック検出など、複雑なロジックを処理します。
cmd/gc
(Goコンパイラ) の役割
cmd/gc
はGoコンパイラの主要部分であり、Goのソースコードを中間表現に変換し、最終的に機械語コードを生成します。このプロセスの中で、Goの言語機能(例えばチャネル操作)は、ランタイムライブラリの特定の関数呼び出しに「ウォーク」(変換)されます。src/cmd/gc/walk.c
や src/cmd/gc/select.c
は、このウォークフェーズを担当するファイルであり、Goの構文木を走査して、より低レベルなランタイム関数呼び出しに置き換える処理を行います。
技術的詳細
このコミットの核心は、Goランタイムのチャネル関連C関数から可変引数(...
)を排除し、代わりに明示的なポインタ引数(byte *v
)を使用するように変更した点です。これに伴い、Goコンパイラ側も、これらのランタイム関数を呼び出す際に、値そのものではなく、値へのポインタを渡すようにコード生成ロジックを修正しています。
具体的には、以下のランタイム関数が変更されました。
runtime·chansend1
: チャネルへの単一要素送信runtime·chanrecv1
: チャネルからの単一要素受信(値のみ)runtime·chanrecv2
: チャネルからの単一要素受信(値と受信成功フラグ)runtime·selectnbsend
:select
文での非ブロック送信runtime·newselect
:select
文の内部構造体初期化
これらの関数は、以前はチャネル要素を可変引数として受け取っていました。例えば、runtime·chansend1(ChanType *t, Hchan* c, ...)
のように定義されていました。この ...
の部分がGCにとって問題でした。
変更後、これらの関数は byte *v
のような明示的なポインタ引数を受け取るようになりました。例えば、runtime·chansend1(ChanType *t, Hchan* c, byte *v)
となります。byte *v
は、送信または受信されるチャネル要素のメモリ上のアドレスを指します。
コンパイラ側(src/cmd/gc/walk.c
と src/cmd/gc/select.c
)では、Goのソースコードでチャネル操作が記述されている箇所を変換する際に、以下の処理が追加されました。
- 値からポインタへの変換: チャネルに送信する値や、チャネルから受信した値を格納する場所について、その値そのものではなく、その値が格納されているメモリのアドレス(ポインタ)を取得するように変更されました。
- 一時変数の導入: リテラル値や複雑な式の結果など、直接アドレスが取れないような値の場合、一時変数を導入し、その一時変数に値を格納した後、一時変数のアドレスをランタイム関数に渡すようにコンパイラがコードを生成します。これにより、常にポインタを渡すという一貫したインターフェースが保証されます。
chanrecv2
の変更:chanrecv2
は以前、受信した要素と成功フラグを両方とも戻り値として返していました。この変更により、要素はポインタ引数として渡され、成功フラグのみが戻り値として返されるようになりました。コンパイラはこれに合わせて、多値代入(a, ok := <-ch
)の処理を調整しています。newselect
の戻り値変更:newselect
関数は、以前はSelect**selp
という引数を通じてSelect
構造体へのポインタを間接的に返していましたが、変更後はSelect*
を直接戻り値として返すようになりました。これは可変引数とは直接関係ありませんが、ランタイム関数のインターフェースをより直接的にする改善です。
これらの変更により、GCはスタックをスキャンする際に、チャネル関連のランタイム関数呼び出しの引数が常に明確なポインタ型であることを認識できるようになります。これにより、GCはスタック上のポインタを正確に識別し、ヒープ上のオブジェクトを適切に追跡できるようになり、GCの正確性と信頼性が大幅に向上します。
コアとなるコードの変更箇所
src/cmd/gc/builtin.c
および src/cmd/gc/runtime.go
の関数シグネチャ変更
--- a/src/cmd/gc/builtin.c
+++ b/src/cmd/gc/builtin.c
@@ -77,11 +77,11 @@ char *runtimeimport =
"func @\".mapiternext (@\".hiter·1 *any)\\n"
"func @\".makechan (@\".chanType·2 *byte, @\".hint·3 int64) (@\".hchan·1 chan any)\\n"
-" func @\".chanrecv1 (@\".chanType·2 *byte, @\".hchan·3 <-chan any) (@\".elem·1 any)\\n"
-" func @\".chanrecv2 (@\".chanType·3 *byte, @\".hchan·4 <-chan any) (@\".elem·1 any, @\".received·2 bool)\\n"
-" func @\".chansend1 (@\".chanType·1 *byte, @\".hchan·2 chan<- any, @\".elem·3 any)\\n"
+" func @\".chanrecv1 (@\".chanType·1 *byte, @\".hchan·2 <-chan any, @\".elem·3 *any)\\n"
+" func @\".chanrecv2 (@\".chanType·2 *byte, @\".hchan·3 <-chan any, @\".elem·4 *any) (? bool)\\n"
+" func @\".chansend1 (@\".chanType·1 *byte, @\".hchan·2 chan<- any, @\".elem·3 *any)\\n"
"func @\".closechan (@\".hchan·1 any)\\n"
-" func @\".selectnbsend (@\".chanType·2 *byte, @\".hchan·3 chan<- any, @\".elem·4 any) (? bool)\\n"
+" func @\".selectnbsend (@\".chanType·2 *byte, @\".hchan·3 chan<- any, @\".elem·4 *any) (? bool)\\n"
"func @\".selectnbrecv (@\".chanType·2 *byte, @\".elem·3 *any, @\".hchan·4 <-chan any) (? bool)\\n"
"func @\".selectnbrecv2 (@\".chanType·2 *byte, @\".elem·3 *any, @\".received·4 *bool, @\".hchan·5 <-chan any) (? bool)\\n"
"func @\".newselect (@\".size·2 int32) (@\".sel·1 *byte)\\n"
--- a/src/cmd/gc/runtime.go
+++ b/src/cmd/gc/runtime.go
@@ -101,12 +101,12 @@ func mapiternext(hiter *any)
// *byte is really *runtime.Type
func makechan(chanType *byte, hint int64) (hchan chan any)
-func chanrecv1(chanType *byte, hchan <-chan any) (elem any)
-func chanrecv2(chanType *byte, hchan <-chan any) (elem any, received bool)
-func chansend1(chanType *byte, hchan chan<- any, elem any)
+func chanrecv1(chanType *byte, hchan <-chan any, elem *any)
+func chanrecv2(chanType *byte, hchan <-chan any, elem *any) bool
+func chansend1(chanType *byte, hchan chan<- any, elem *any)
func closechan(hchan any)
-func selectnbsend(chanType *byte, hchan chan<- any, elem any) bool
+func selectnbsend(chanType *byte, hchan chan<- any, elem *any) bool
func selectnbrecv(chanType *byte, elem *any, hchan <-chan any) bool
func selectnbrecv2(chanType *byte, elem *any, received *bool, hchan <-chan any) bool
src/pkg/runtime/chan.c
のランタイム関数実装変更
--- a/src/pkg/runtime/chan.c
+++ b/src/pkg/runtime/chan.c
@@ -430,35 +430,31 @@ closed:
runtime·blockevent(mysg.releasetime - t0, 2);
}
-// chansend1(hchan *chan any, elem any);\n
+// chansend1(hchan *chan any, elem *any);\n
#pragma textflag NOSPLIT
void
-runtime·chansend1(ChanType *t, Hchan* c, ...)\n
+runtime·chansend1(ChanType *t, Hchan* c, byte *v)\n
{\n-\truntime·chansend(t, c, (byte*)(&c+1), nil, runtime·getcallerpc(&t));\n
+\truntime·chansend(t, c, v, nil, runtime·getcallerpc(&t));\n
}\n \n-// chanrecv1(hchan *chan any) (elem any);\n
+// chanrecv1(hchan *chan any, elem *any);\n
#pragma textflag NOSPLIT
void
-runtime·chanrecv1(ChanType *t, Hchan* c, ...)\n
+runtime·chanrecv1(ChanType *t, Hchan* c, byte *v)\n
{\n-\truntime·chanrecv(t, c, (byte*)(&c+1), nil, nil);\n
+\truntime·chanrecv(t, c, v, nil, nil);\n
}\n \n-// chanrecv2(hchan *chan any) (elem any, received bool);\n
+// chanrecv2(hchan *chan any, elem *any) (received bool);\n
#pragma textflag NOSPLIT
void
-runtime·chanrecv2(ChanType *t, Hchan* c, ...)\n
+runtime·chanrecv2(ChanType *t, Hchan* c, byte *v, bool received)\n
{\n-\tbyte *ae, *ap;\n-\n-\tae = (byte*)(&c+1);\n-\tap = ae + t->elem->size;\n-\truntime·chanrecv(t, c, ae, nil, ap);\n+\truntime·chanrecv(t, c, v, nil, &received);\n }\n \n-// func selectnbsend(c chan any, elem any) bool\n+// func selectnbsend(c chan any, elem *any) bool\n //\n // compiler implements\n //\n@@ -479,13 +475,9 @@ runtime·chanrecv2(ChanType *t, Hchan* c, ...)\n //\n #pragma textflag NOSPLIT\n void
-runtime·selectnbsend(ChanType *t, Hchan *c, ...)\n+runtime·selectnbsend(ChanType *t, Hchan *c, byte *val, bool pres)\n {\n-\tbyte *ae, *ap;\n-\n-\tae = (byte*)(&c + 1);\n-\tap = ae + ROUND(t->elem->size, Structrnd);\n-\truntime·chansend(t, c, ae, ap, runtime·getcallerpc(&t));\n+\truntime·chansend(t, c, val, &pres, runtime·getcallerpc(&t));\n }\n \n // func selectnbrecv(elem *any, c chan any) bool
src/cmd/gc/walk.c
および src/cmd/gc/select.c
のコンパイラ変換ロジック変更
walk.c
の ORECV
(受信) および OSEND
(送信) の処理部分:
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -163,7 +163,6 @@ walkstmt(Node **np)
case OCALLFUNC:
case ODELETE:
case OSEND:
-\tcase ORECV:
case OPRINT:
case OPRINTN:
case OPANIC:
@@ -179,6 +178,21 @@ walkstmt(Node **np)
\t\tn->op = OEMPTY; // don\'t leave plain values as statements.\n
\t\tbreak;\n
+\tcase ORECV:\n
+\t\t// special case for a receive where we throw away\n
+\t\t// the value received.\n
+\t\tif(n->typecheck == 0)\n
+\t\t\tfatal(\"missing typecheck: %+N\", n);\n
+\t\tinit = n->ninit;\n
+\t\tn->ninit = nil;\n
+\n+\t\twalkexpr(&n->left, &init);\n
+\t\tn = mkcall1(chanfn(\"chanrecv1\", 2, n->left->type), T, &init, typename(n->left->type), n->left, nodnil());\n
+\t\twalkexpr(&n, &init);\n+\n+\t\taddinit(&n, init);\n+\t\tbreak;\n+\n \tcase OBREAK:\n \tcase ODCL:\n \tcase OCONTINUE:\
...
@@ -1149,8 +1165,12 @@ walkexpr(Node **np, NodeList **init)
case ORECV:
\t\twalkexpr(&n->left, init);\n
-\t\t\twalkexpr(&n->right, init);\n
-\t\t\tn = mkcall1(chanfn(\"chanrecv1\", 2, n->left->type), n->type, init, typename(n->left->type), n->left);\n
+\t\t\tvar = temp(n->left->type->type);\n
+\t\t\tn1 = nod(OADDR, var, N);\n
+\t\t\tn = mkcall1(chanfn(\"chanrecv1\", 2, n->left->type), T, init, typename(n->left->type), n->left, n1);\n
+\t\t\twalkexpr(&n, init);\n
+\t\t\t*init = list(*init, n);\n
+\t\t\tn = var;\n
\t\tgoto ret;\n
case OSLICE:\
@@ -1427,7 +1447,19 @@ walkexpr(Node **np, NodeList **init)
\t\tgoto ret;\n
case OSEND:\n
-\t\t\tn = mkcall1(chanfn(\"chansend1\", 2, n->left->type), T, init, typename(n->left->type), n->left, n->right);\n
+\t\t\tn1 = n->right;\n
+\t\t\tn1 = assignconv(n1, n->left->type->type, \"chan send\");\n
+\t\t\twalkexpr(&n1, init);\n
+\t\t\tif(islvalue(n1)) {\n
+\t\t\t\tn1 = nod(OADDR, n1, N);\n
+\t\t\t} else {\n
+\t\t\t\tvar = temp(n1->type);\n
+\t\t\t\tn1 = nod(OAS, var, n1);\n
+\t\t\t\ttypecheck(&n1, Etop);\n
+\t\t\t\t*init = list(*init, n1);\n
+\t\t\t\tn1 = nod(OADDR, var, N);\n
+\t\t\t}\n
+\t\t\tn = mkcall1(chanfn(\"chansend1\", 2, n->left->type), T, init, typename(n->left->type), n->left, n1);\n
\t\tgoto ret;\n
case OCLOSURE:\
walk.c
の OAS2FUNC
(多値代入) の処理部分 (chanrecv2
関連):
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -645,11 +659,13 @@ walkexpr(Node **np, NodeList **init)
\t\tr = n->rlist->n;\n
\t\twalkexprlistsafe(n->list, init);\n
\t\twalkexpr(&r->left, init);\n
+\t\t\tvar = temp(r->left->type->type);\n
+\t\t\tn1 = nod(OADDR, var, N);\n
\t\tfn = chanfn(\"chanrecv2\", 2, r->left->type);\n
-\t\t\tr = mkcall1(fn, getoutargx(fn->type), init, typename(r->left->type), r->left);\n
-\t\t\tn->rlist->n = r;\n
-\t\t\tn->op = OAS2FUNC;\n
-\t\t\tgoto as2func;\n
+\t\t\tr = mkcall1(fn, types[TBOOL], init, typename(r->left->type), r->left, n1);\n
+\t\t\tn->op = OAS2;\n
+\t\t\tn->rlist = concat(list1(var), list1(r));\n
+\t\t\tgoto as2;\n
case OAS2MAPR:\n
\t\t// a,b = m[i];\
コアとなるコードの解説
src/cmd/gc/builtin.c
および src/cmd/gc/runtime.go
これらのファイルは、Goコンパイラが認識するランタイム関数のシグネチャを定義しています。変更前は、chanrecv1
, chanrecv2
, chansend1
, selectnbsend
の elem
引数が any
型(Go側)または可変引数(C側)として宣言されていました。これは、Goのインターフェース型 any
が内部的には値のコピーを伴うため、GCがスタック上のポインタを追跡する上で曖昧さをもたらしていました。
変更後、これらの関数の elem
引数は *any
型(Go側)または *byte
型(C側)に変更されました。これは、チャネル要素そのものではなく、その要素が格納されているメモリのアドレス(ポインタ)を渡すことを意味します。これにより、GCはスタック上のこの引数が常にポインタであることを明確に認識でき、そのポインタが指すヒープ上のオブジェクトを安全に追跡できるようになります。
特に chanrecv2
は、以前は (elem any, received bool)
のように多値を返していましたが、変更後は (elem *any)
を引数として受け取り、bool
のみを戻り値として返すようになりました。これは、受信した要素をポインタ経由で書き込み、受信成功の有無だけを戻り値で伝えるという、よりGCフレンドリーなインターフェースへの変更です。
src/pkg/runtime/chan.c
このファイルは、Goのチャネル操作の実際のランタイム実装を含んでいます。変更前は、runtime·chansend1
などの関数は ...
を使用して可変引数を受け取っていました。そして、(&c+1)
のようなポインタ演算を使ってスタック上の引数にアクセスしていました。これはCの可変引数の典型的なアクセス方法ですが、GCにとっては不透明でした。
変更後、これらの関数は byte *v
のような明示的なポインタ引数を受け取るようになりました。これにより、関数は v
を直接使用してチャネル要素のメモリにアクセスできます。例えば、runtime·chansend(t, c, v, nil, runtime·getcallerpc(&t));
のように、v
が直接 runtime·chansend
に渡されます。この変更により、GCはこれらの関数のスタックフレームをスキャンする際に、v
がポインタであることを確実に認識し、その指すメモリを追跡できるようになります。
また、newselect
関数は、以前は Select **selp
という引数を通じて Select
構造体へのポインタを間接的に返していましたが、変更後は Select*
を直接戻り値として返すようになりました。これは、可変引数とは直接関係ありませんが、ランタイム関数のインターフェースをより直接的にし、呼び出し側のコードを簡素化する改善です。
src/cmd/gc/walk.c
および src/cmd/gc/select.c
これらのファイルはGoコンパイラのバックエンドの一部であり、Goのソースコードの抽象構文木(AST)を走査し、ランタイム関数呼び出しに変換する役割を担っています。
ORECV
(受信): 以前は、受信操作が単独のステートメントとして現れる場合(例:<-ch
)、その受信値は破棄されていました。変更後、コンパイラは一時変数var
を作成し、そのアドレス&var
をchanrecv1
に渡すようにコードを生成します。これにより、受信した値は一時変数に書き込まれ、GCはその一時変数を介してポインタを追跡できます。OSEND
(送信): チャネルに値を送信する際、コンパイラは送信される値n->right
が直接アドレスを取れるlvalueであるか、そうでないかをチェックします。lvalueでない場合(例: リテラルや式の結果)、一時変数var
を作成し、その値n1
をvar
に代入した後、&var
をchansend1
に渡します。これにより、常にポインタがランタイム関数に渡されることが保証されます。OAS2FUNC
(多値代入): 特にa, ok := <-ch
のようなchanrecv2
を伴う多値代入の場合、コンパイラは受信した要素を格納するための一時変数var
を作成し、そのアドレス&var
をchanrecv2
に渡します。chanrecv2
はbool
の結果を返すため、コンパイラはOAS2
ノードを構築して、一時変数var
とchanrecv2
の戻り値(bool
)をそれぞれ代入先の変数に割り当てます。
これらのコンパイラ側の変更は、ランタイム関数のシグネチャ変更と密接に連携しており、Goのソースコードで記述されたチャネル操作が、GCにとって安全なポインタベースのランタイム呼び出しに正しく変換されることを保証します。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goのチャネルに関する公式ブログ記事 (Go Concurrency Patterns: Pipelines and Cancellation): https://blog.golang.org/pipelines
- Goのガベージコレクションに関する詳細 (Go's Garbage Collector: From 1.5 to 1.8): https://blog.golang.org/go15gc
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/gc/
,src/pkg/runtime/
) - C言語の可変引数に関する一般的な知識
- ガベージコレクションの原理に関する一般的な知識