[インデックス 18032] ファイルの概要
このコミットは、Goランタイムにおけるインターフェース変換ルーチン(convT2E
および convT2I
)の呼び出し方法を変更し、可変長引数(vararg)C呼び出しの使用を廃止し、代わりに参照渡し(ポインタ渡し)を採用するものです。これにより、Goランタイムの内部実装が簡素化され、効率と移植性が向上します。
コミット
commit deb554934c0c2a87b24f4eb29cdcbbd4ea68a6d6
Author: Keith Randall <khr@golang.org>
Date: Tue Dec 17 16:55:06 2013 -0800
runtime, gc: call interface conversion routines by reference.
Part of getting rid of vararg C calls.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/23310043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/deb554934c0c2a87b24f4eb29cdcbbd4ea68a6d6
元コミット内容
Goランタイムとガベージコレクタ(gc)において、インターフェース変換ルーチンを可変長引数C呼び出しではなく、参照渡しで呼び出すように変更します。これは、可変長引数C呼び出しを廃止する取り組みの一環です。
変更の背景
このコミットの主な背景は、GoランタイムがC言語の可変長引数(vararg)関数呼び出しへの依存を減らすという、より大きな戦略の一部です。C言語の可変長引数関数は、引数の渡し方がプラットフォームに依存し、固定引数関数に比べてパフォーマンスが劣る可能性があり、最適化が難しい場合があります。また、GoからCの可変長引数関数を呼び出す場合、引数をスタックに手動で配置する必要があり、これは複雑でエラーが発生しやすい処理です。
convT2E
(型から空インターフェースへの変換)と convT2I
(型から非空インターフェースへの変換)のようなインターフェース変換ルーチンは、Goの型システムの中核をなす部分であり、頻繁に呼び出されます。これらのルーチンを可変長引数呼び出しから固定引数(参照渡し)に変更することで、呼び出し規約が標準化され、より効率的で移植性の高いコード生成が可能になります。これにより、ランタイムの堅牢性が向上し、将来的な最適化の道が開かれます。
前提知識の解説
- Goのインターフェース: Goのインターフェースは、メソッドのシグネチャの集合を定義する型です。Goのインターフェース値は、内部的に「型情報」と「データ」の2つのポインタで構成されます。
- 空インターフェース (
interface{}
またはany
): 任意の型の値を保持できるインターフェース。型情報とデータポインタのみを持ちます。 - 非空インターフェース: 特定のメソッドセットを定義するインターフェース。型情報、データポインタに加えて、
itab
(インターフェーステーブル)へのポインタを持ちます。
- 空インターフェース (
itab
(Interface Table): Goランタイムの内部データ構造で、特定の具象型が特定のインターフェースをどのように実装しているか(どのメソッドがどの関数に対応するか)を記述します。これにより、インターフェースメソッドの動的なディスパッチが可能になります。convT2E
: 具象型から空インターフェースへの変換を行うランタイム関数。convT2I
: 具象型から非空インターフェースへの変換を行うランタイム関数。この変換では、具象型がインターフェースを満たすかどうかをチェックし、必要に応じてitab
を検索または構築します。- C言語の可変長引数 (Variadic Arguments):
printf
のように、引数の数が可変である関数を定義するためのC言語の機能。stdarg.h
ヘッダのva_list
,va_start
,va_arg
,va_end
マクロを使用して引数にアクセスします。これらの呼び出しは、固定引数関数に比べてオーバーヘッドが大きく、コンパイラの最適化が難しい場合があります。 - 参照渡し (Pass by Reference): 関数の引数として変数のメモリアドレス(ポインタ)を渡すこと。これにより、関数は元の変数のメモリ位置に直接アクセスし、その値を読み書きできます。大きなデータ構造を渡す際に、コピーのオーバーヘッドを避けるために効率的です。
- 値渡し (Pass by Value): 関数の引数として変数のコピーを渡すこと。関数はコピーに対して操作を行い、元の変数には影響を与えません。
技術的詳細
このコミットは、Goコンパイラ(cmd/gc
)とGoランタイム(pkg/runtime
)の両方にわたる協調的な変更を伴います。
-
コンパイラ側の変更 (
src/cmd/gc/builtin.c
,src/cmd/gc/runtime.go
,src/cmd/gc/walk.c
):- 関数シグネチャの変更:
src/cmd/gc/builtin.c
とsrc/cmd/gc/runtime.go
では、convT2E
とconvT2I
のGo側の宣言が変更され、変換対象の要素 (elem
) がany
型(Goの空インターフェース)から*any
型(空インターフェースへのポインタ)を受け取るように修正されました。これは、Cランタイム関数がポインタを受け取るようになったことに対応しています。 - コード生成の変更:
src/cmd/gc/walk.c
は、Goコンパイラの「ウォーク」フェーズ(抽象構文木 (AST) をコード生成に適した形式に変換する段階)を担当します。このファイルへの変更が最も重要です。- 以前は、変換対象の要素が直接引数として渡されていました。
- 変更後、
convT2E
やconvT2I
を呼び出す際、変換対象の要素がインターフェース型でない(つまり、具象型である)場合、コンパイラは明示的にその要素のアドレス(OADDR
ノード)を取得して渡すようになりました。 - もし変換対象の要素がlvalue(アドレスを持つことができる変数など)でない場合(例:一時的な式の結果)、コンパイラはまずその値を一時変数に格納し、その一時変数のアドレスを渡すように変更されました。これにより、常に有効でアドレス可能なメモリ位置がランタイム関数に渡されることが保証されます。
- 関数シグネチャの変更:
-
ランタイム側の変更 (
src/pkg/runtime/iface.c
):- 可変長引数の廃止:
runtime·convT2I
とruntime·convT2E
のC言語実装から、可変長引数を示す...
が削除されました。代わりに、変換対象の要素はbyte *elem
として明示的なポインタ引数で受け取られるようになりました。 - 引数アクセスの簡素化: 以前は、可変長引数リストから
elem
やret
のポインタを計算するために、(&cache+1)
や(elem + ROUND(wid, Structrnd))
のような手動のスタックポインタ操作とサイズ計算が必要でした。この変更により、これらの複雑な計算が不要になり、elem
ポインタが直接変換対象のデータへのポインタとして使用できるようになりました。 copyin
の直接利用:copyin
関数は、ソース要素のデータをインターフェースのデータフィールドにコピーする役割を担います。この変更後、copyin
は直接渡されたelem
ポインタを利用できるようになり、コードがより直接的で読みやすくなりました。
- 可変長引数の廃止:
この一連の変更により、Goのインターフェース変換メカニズムは、C言語の可変長引数呼び出しの複雑さとオーバーヘッドから解放され、より効率的で保守しやすい実装へと進化しました。
コアとなるコードの変更箇所
src/cmd/gc/builtin.c
--- a/src/cmd/gc/builtin.c
+++ b/src/cmd/gc/builtin.c
@@ -42,8 +42,8 @@ char *runtimeimport =
"func @\".typ2Itab (@\".typ·2 *byte, @\".typ2·3 *byte, @\".cache·4 **byte) (@\".ret·1 *byte)\n"
"func @\".convI2E (@\".elem·2 any) (@\".ret·1 any)\n"
"func @\".convI2I (@\".typ·2 *byte, @\".elem·3 any) (@\".ret·1 any)\n"
- "func @\".convT2E (@\".typ·2 *byte, @\".elem·3 any) (@\".ret·1 any)\n"
- "func @\".convT2I (@\".typ·2 *byte, @\".typ2·3 *byte, @\".cache·4 **byte, @\".elem·5 any) (@\".ret·1 any)\n"
+ "func @\".convT2E (@\".typ·2 *byte, @\".elem·3 *any) (@\".ret·1 any)\n"
+ "func @\".convT2I (@\".typ·2 *byte, @\".typ2·3 *byte, @\".cache·4 **byte, @\".elem·5 *any) (@\".ret·1 any)\n"
"func @\".assertE2E (@\".typ·2 *byte, @\".iface·3 any) (@\".ret·1 any)\n"
"func @\".assertE2E2 (@\".typ·3 *byte, @\".iface·4 any) (@\".ret·1 any, @\".ok·2 bool)\n"
"func @\".assertE2I (@\".typ·2 *byte, @\".iface·3 any) (@\".ret·1 any)\n"
src/cmd/gc/runtime.go
--- a/src/cmd/gc/runtime.go
+++ b/src/cmd/gc/runtime.go
@@ -58,8 +58,8 @@ func slicestringcopy(to any, fr any) int
func typ2Itab(typ *byte, typ2 *byte, cache **byte) (ret *byte)
func convI2E(elem any) (ret any)
func convI2I(typ *byte, elem any) (ret any)
-func convT2E(typ *byte, elem any) (ret any)
-func convT2I(typ *byte, typ2 *byte, cache **byte, elem any) (ret any)
+func convT2E(typ *byte, elem *any) (ret any)
+func convT2I(typ *byte, typ2 *byte, cache **byte, elem *any) (ret any)
// interface type assertions x.(T)
func assertE2E(typ *byte, iface any) (ret any)
src/cmd/gc/walk.c
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -898,7 +898,20 @@ walkexpr(Node **np, NodeList **init)
if(isinter(n->left->type)) {
ll = list(ll, n->left);
} else {
- // regular types are passed by reference to avoid C vararg calls
+ // regular types are passed by reference to avoid C vararg calls
+ if(islvalue(n->left)) {
+ ll = list(ll, nod(OADDR, n->left, N));
+ } else {
+ var = temp(n->left->type);
+ n1 = nod(OAS, var, n->left);
+ typecheck(&n1, Etop);
+ *init = list(*init, n1);
+ ll = list(ll, nod(OADDR, var, N));
+ }
+ }
}
argtype(fn, n->left->type);
argtype(fn, n->type);
dowidth(fn->type);
src/pkg/runtime/iface.c
--- a/src/pkg/runtime/iface.c
+++ b/src/pkg/runtime/iface.c
@@ -183,42 +183,31 @@ runtime·typ2Itab(Type *t, InterfaceType *inter, Itab **cache, Itab *ret)
FLUSH(&ret);
}
-// func convT2I(typ *byte, typ2 *byte, cache **byte, elem any) (ret any)
+// func convT2I(typ *byte, typ2 *byte, cache **byte, elem *any) (ret any)
#pragma textflag NOSPLIT
void
-runtime·convT2I(Type *t, InterfaceType *inter, Itab **cache, ...)
+runtime·convT2I(Type *t, InterfaceType *inter, Itab **cache, byte *elem, Iface ret)
{
-\tbyte *elem;\n-\tIface *ret;
\tItab *tab;
-\tint32 wid;
\n-\telem = (byte*)(&cache+1);
-\twid = t->size;
-\tret = (Iface*)(elem + ROUND(wid, Structrnd));
\ttab = runtime·atomicloadp(cache);
\tif(!tab) {
\t\ttab = itab(inter, t, 0);
\t\truntime·atomicstorep(cache, tab);
\t}
-\tret->tab = tab;
-\tcopyin(t, elem, &ret->data);
+\tret.tab = tab;
+\tcopyin(t, elem, &ret.data);
+\tFLUSH(&ret);
}
-// func convT2E(typ *byte, elem any) (ret any)
+// func convT2E(typ *byte, elem *any) (ret any)
#pragma textflag NOSPLIT
void
-runtime·convT2E(Type *t, ...)
+runtime·convT2E(Type *t, byte *elem, Eface ret)
{
-\tbyte *elem;\n-\tEface *ret;
-\tint32 wid;
-\n-\telem = (byte*)(&t+1);
-\twid = t->size;
-\tret = (Eface*)(elem + ROUND(wid, Structrnd));
-\tret->type = t;
-\tcopyin(t, elem, &ret->data);
+\tret.type = t;
+\tcopyin(t, elem, &ret.data);
+\tFLUSH(&ret);
}
static void assertI2Tret(Type *t, Iface i, byte *ret);
コアとなるコードの解説
src/cmd/gc/builtin.c
とsrc/cmd/gc/runtime.go
: これらのファイルは、Goコンパイラが認識するランタイム関数のシグネチャを定義しています。変更は、convT2E
とconvT2I
のelem
引数がany
から*any
になったことを反映しています。これは、Goの型システムにおける変更であり、実際のC言語のポインタ渡しに対応します。src/cmd/gc/walk.c
:- このファイルは、Goのソースコードがコンパイルされる過程で、抽象構文木(AST)を走査し、低レベルの表現に変換する役割を担っています。
- 変更されたコードブロックは、
convT2E
やconvT2I
のようなインターフェース変換関数を呼び出す際の引数処理を制御します。 if(isinter(n->left->type))
の条件は、変換対象の左辺値(n->left
)が既にインターフェース型であるかどうかをチェックします。else
ブロックが今回の変更の核心です。具象型をインターフェースに変換する場合、以前は値を直接渡していましたが、この変更により、その値のアドレスを渡すように修正されました。if(islvalue(n->left))
は、n->left
がアドレスを持つことができる(例えば、変数)かどうかをチェックします。もしそうであれば、nod(OADDR, n->left, N)
を使ってそのアドレスを直接取得します。else
ブロックは、n->left
がlvalueではない場合(例えば、一時的な計算結果など)を処理します。この場合、一時変数 (var = temp(n->left->type)
) を作成し、その一時変数に値を代入 (n1 = nod(OAS, var, n->left)
) してから、その一時変数のアドレス (nod(OADDR, var, N)
) を取得して渡します。これにより、常に安定したメモリ位置へのポインタがランタイム関数に渡されることが保証されます。
src/pkg/runtime/iface.c
:- このファイルは、Goランタイムのインターフェース関連のC言語実装を含んでいます。
runtime·convT2I
とruntime·convT2E
の関数シグネチャが変更され、...
(可変長引数)が削除され、byte *elem
という明示的なポインタ引数を受け取るようになりました。- これにより、関数内部で引数を手動で解析する必要がなくなり、
elem
ポインタが直接変換対象のデータへのポインタとして利用できるようになりました。 - 以前のコードにあった
elem = (byte*)(&cache+1);
やret = (Iface*)(elem + ROUND(wid, Structrnd));
といった、スタック上の引数を手動で計算してアクセスするロジックが削除されました。これは、引数が固定位置にポインタとして渡されるようになったため、不要になったためです。 copyin(t, elem, &ret.data);
のように、copyin
関数が直接elem
ポインタを利用できるようになり、コードがより簡潔になりました。
これらの変更は、Goコンパイラとランタイム間のインターフェース変換に関する契約を更新し、より効率的で標準的なC呼び出し規約を利用することで、パフォーマンスと保守性を向上させています。
関連リンク
- Go言語のインターフェースに関する公式ドキュメントやブログ記事(当時のもの)
- Goのコンパイラ(
cmd/gc
)の内部構造に関する資料 - Goランタイム(
pkg/runtime
)のインターフェース実装に関する資料
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/gc
とsrc/pkg/runtime
ディレクトリ) - C言語の可変長引数に関するドキュメント
- Go言語のインターフェースの内部表現に関する技術記事やGoの公式ブログ
- Goのコミット履歴と関連するコードレビュー(
https://golang.org/cl/23310043
)