[インデックス 15379] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)とランタイムにおけるクロージャの実装に関する重要な変更を導入しています。特に、ARMアーキテクチャでのコンテキストレジスタの変更と、クロージャのためのランタイムコード生成の回避に焦点を当てています。これにより、Go 1.1におけるクロージャの効率性とガベージコレクションのサポートが改善されています。
コミット
commit 9f647288efecb0522df319969bdc82c34d36880a
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 22 14:25:50 2013 -0500
cmd/gc: avoid runtime code generation for closures
Change ARM context register to R7, to get out of the way
of the register allocator during the compilation of the
prologue statements (it wants to use R0 as a temporary).
Step 2 of http://golang.org/s/go11func.
R=ken2
CC=golang-dev
https://golang.org/cl/7369048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9f647288efecb0522df319969bdc82c34d36880a
元コミット内容
cmd/gc: avoid runtime code generation for closures
Change ARM context register to R7, to get out of the way
of the register allocator during the compilation of the
prologue statements (it wants to use R0 as a temporary).
Step 2 of http://golang.org/s/go11func.
変更の背景
このコミットは、Go 1.1におけるクロージャの実装改善の一環として行われました。Go 1.0では、クロージャがキャプチャした変数を扱うために、実行時に動的にコードを生成するアプローチが取られていました。しかし、この方法はいくつかの問題を抱えていました。
- ガベージコレクションの複雑性: 動的に生成されたコードは、ガベージコレクタが正確にポインタを識別し、ヒープ上のオブジェクトを追跡するのを困難にしました。これにより、ガベージコレクションの効率が低下したり、誤ったメモリ解放が発生するリスクがありました。
- パフォーマンスオーバーヘッド: 実行時のコード生成は、クロージャが作成されるたびにオーバーヘッドを発生させ、アプリケーションの起動時間や実行時のパフォーマンスに影響を与える可能性がありました。
- デバッグの困難性: 動的に生成されたコードは、デバッグツールからの可視性が低く、問題の特定と解決を困難にしました。
- ARMアーキテクチャ固有の問題: ARMアーキテクチャでは、プロローグ(関数の冒頭部分)のコンパイル時にレジスタアロケータがR0レジスタを一時的に使用しようとするため、クロージャのコンテキストレジスタがR0と競合する問題がありました。
このコミットは、これらの問題を解決し、特にガベージコレクションのサポートを改善するために、クロージャのランタイムコード生成を回避する新しいアプローチを導入しています。また、ARMアーキテクチャにおけるレジスタ競合の問題も同時に解決しています。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が必要です。
- クロージャ (Closure): クロージャとは、関数が定義された環境(レキシカル環境)を記憶し、その環境内の変数にアクセスできる関数のことです。Go言語では、関数内で別の関数を定義し、外側の関数のローカル変数をキャプチャすることができます。
- ガベージコレクション (Garbage Collection, GC): プログラムが動的に確保したメモリ領域のうち、もはや使用されなくなった領域を自動的に解放する仕組みです。GoのGCは、ヒープ上のポインタを正確に識別し、到達可能なオブジェクトをマークすることで動作します。
- レジスタアロケータ (Register Allocator): コンパイラの最適化フェーズの一部で、プログラムの変数をCPUのレジスタに割り当てる役割を担います。レジスタはメモリよりも高速にアクセスできるため、レジスタに割り当てることでプログラムの実行速度を向上させます。
- プロローグ (Prologue): 関数の実行開始時に行われる一連の処理のことです。これには、スタックフレームのセットアップ、レジスタの保存、引数の処理などが含まれます。
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担います。 - Goランタイム (
src/pkg/runtime
): Goプログラムの実行をサポートするライブラリ群です。ガベージコレクション、スケジューラ、プリミティブな操作などが含まれます。 - ARMアーキテクチャ: モバイルデバイスや組み込みシステムで広く使用されているCPUアーキテクチャです。レジスタの利用方法や命令セットに特徴があります。
unsafe.Pointer
: Go言語における特殊なポインタ型で、任意の型のポインタを保持できます。Goの型システムをバイパスしてメモリを直接操作するために使用されますが、誤用するとメモリ安全性の問題を引き起こす可能性があります。- 複合リテラル (Composite Literal): 構造体、配列、スライス、マップなどの複合型を初期化するための構文です。
技術的詳細
このコミットの主要な変更点は、クロージャのランタイムコード生成を廃止し、代わりにコンパイル時にクロージャの構造を定義するアプローチに切り替えたことです。
変更前(Go 1.0):
Go 1.0では、クロージャがキャプチャした変数を参照するために、runtime.closure
という関数が使用されていました。この関数は、実行時に動的に機械語コードを生成し、キャプチャされた変数へのアクセスを処理していました。この動的コード生成は、ガベージコレクタがクロージャの内部構造を理解するのを困難にし、ポインタの正確な追跡を妨げていました。
変更後(Go 1.1以降):
このコミットでは、runtime.closure
関数が削除され、クロージャの表現方法が大きく変更されました。
-
クロージャの構造体表現: クロージャは、コンパイル時に匿名構造体として表現されるようになりました。この構造体は、クロージャの関数ポインタと、キャプチャされた変数へのポインタ(
unsafe.Pointer
)をフィールドとして持ちます。例えば、int i
とstring s
をキャプチャするクロージャの場合、以下のような構造体が生成されます。struct { F uintptr // 関数ポインタ A0 *int // キャプチャされた変数 i へのポインタ A1 *string // キャプチャされた変数 s へのポインタ }
この構造体は、ガベージコレクタがクロージャ内のポインタを正確に識別できるように、型情報を提供します。これにより、GCはクロージャが参照するヒープ上のオブジェクトを適切に追跡し、メモリリークを防ぐことができます。
-
コンパイル時のコード生成: クロージャの本体は、通常の関数と同様にコンパイル時に機械語コードに変換されます。この関数は、クロージャの構造体からキャプチャされた変数へのポインタを読み取り、それらを使用して変数にアクセスします。
-
ARMコンテキストレジスタの変更: ARMアーキテクチャでは、クロージャのコンテキスト(キャプチャされた変数へのポインタ)を保持するためにR7レジスタが使用されるようになりました。以前はR0レジスタが使用されていましたが、R0はプロローグのコンパイル時にレジスタアロケータが一時的に使用しようとするため、競合が発生していました。R7への変更により、この競合が解消され、コンパイル時の問題が回避されます。
-
OCLOSUREVAR
とOCFUNC
ノードの導入: コンパイラのAST(抽象構文木)に、新しいノードタイプOCLOSUREVAR
とOCFUNC
が導入されました。OCLOSUREVAR
: クロージャ内でキャプチャされた変数への参照を表します。OCFUNC
: C言語の関数ポインタ(Goの関数値ではない)への参照を表します。これは、クロージャの関数ポインタを扱う際に使用されます。
これらの変更により、Go 1.1ではクロージャのガベージコレクションがより効率的かつ正確になり、ランタイムコード生成によるオーバーヘッドがなくなりました。
コアとなるコードの変更箇所
このコミットでは、Goコンパイラ(cmd/gc
)とランタイム(src/pkg/runtime
)の複数のファイルが変更されています。
src/cmd/gc/closure.c
:makeclosure
関数が大幅に書き換えられ、クロージャを匿名構造体として表現し、そのフィールドとしてキャプチャされた変数へのポインタを持つように変更されました。walkclosure
関数も変更され、sys.closure
ランタイム関数を呼び出す代わりに、複合リテラルを使用してクロージャ構造体を直接構築するように変更されました。OCLOSUREVAR
ノードが導入され、クロージャ変数へのアクセスが処理されます。
src/cmd/gc/go.h
:OCLOSUREVAR
とOCFUNC
という新しいASTノードタイプが追加されました。
src/cmd/gc/typecheck.c
:OCLOSUREVAR
とOCFUNC
ノードの型チェックロジックが追加されました。
src/cmd/gc/walk.c
:OCLOSUREVAR
とOCFUNC
ノードのウォークロジックが追加されました。walkcallclosure
のコメントアウトされたコードが削除され、新しいクロージャ生成ロジックが反映されました。
src/cmd/gc/runtime.go
:closure()
ランタイム関数の宣言が削除されました。
src/cmd/5g/ggen.c
,src/cmd/6g/ggen.c
,src/cmd/8g/ggen.c
(各アーキテクチャのコード生成):ginscall
関数が変更され、関数呼び出しの際にレジスタの扱いが調整されました。特にARM (5g
) では、コンテキストレジスタがR7に変更されました。
src/cmd/5g/gsubr.c
,src/cmd/6g/gsubr.c
,src/cmd/8g/gsubr.c
(各アーキテクチャのサブルーチン):naddr
関数にOCLOSUREVAR
とOCFUNC
のケースが追加され、これらのノードがどのようにアドレスに変換されるかが定義されました。ARM (5g
) では、OCLOSUREVAR
がR7レジスタからのオフセットとして扱われます。
src/cmd/5g/peep.c
,src/cmd/6g/peep.c
,src/cmd/8g/peep.c
(各アーキテクチャのピーフホール最適化):copyu
関数にレジスタ関連の最適化ルールが追加されました。
src/pkg/runtime/asm_arm.s
:- ARMアセンブリコードが変更され、クロージャのコンテキストレジスタとしてR7が使用されるように修正されました。
gogocall
,gogocallfn
,morestack
,jmpdefer
などの関数でR0からR7への変更が見られます。
- ARMアセンブリコードが変更され、クロージャのコンテキストレジスタとしてR7が使用されるように修正されました。
src/pkg/runtime/closure_386.c
,src/pkg/runtime/closure_amd64.c
,src/pkg/runtime/closure_arm.c
:- これらのファイルは完全に削除されました。これは、ランタイムでの動的なコード生成が不要になったことを意味します。
コアとなるコードの解説
このコミットの核心は、src/cmd/gc/closure.c
におけるmakeclosure
とwalkclosure
関数の変更、およびsrc/pkg/runtime/closure_*.c
ファイルの削除にあります。
src/cmd/gc/closure.c
の変更:
makeclosure
関数は、Goのクロージャをコンパイル時にどのように表現するかを定義します。変更前は、ランタイムで動的にコードを生成するための準備を行っていましたが、変更後は、クロージャを匿名構造体として定義し、その構造体がクロージャの関数ポインタとキャプチャされた変数へのポインタを保持するようにします。
// 変更後の makeclosure の一部 (概念的な表現)
static Node*
makeclosure(Node *func, int nowrap)
{
Node *xtype, *v, *addr, *xfunc, *cv;
NodeList *l, *body;
static int closgen;
char *p;
int offset;
// ... (既存のコード) ...
// クロージャ変数を保持する匿名構造体のフィールドを定義
// 各クロージャ変数に対して、その型へのポインタを持つフィールドを追加
for(l=func->cvars; l; l=l->next) {
// ... (変数の名前解決など) ...
addr->class = PAUTO; // 自動変数として宣言
// ...
cv = nod(OCLOSUREVAR, N, N); // 新しい OCLOSUREVAR ノード
cv->type = ptrto(v->type); // 変数の型へのポインタ
cv->xoffset = offset; // オフセットを設定
body = list(body, nod(OAS, addr, cv)); // 代入文 (addr = cv)
offset += widthptr; // ポインタのサイズ分オフセットを進める
}
// ... (クロージャの本体の処理) ...
}
walkclosure
関数は、クロージャが使用される箇所で、そのクロージャをどのように構築するかを定義します。変更前はsys.closure
ランタイム関数を呼び出していましたが、変更後は、コンパイル時に匿名構造体の複合リテラルを直接生成するようにします。これにより、ランタイムでの動的なコード生成が不要になります。
// 変更後の walkclosure の一部 (概念的な表現)
Node*
walkclosure(Node *func, NodeList **init)
{
Node *clos, *typ;
NodeList *l;
char buf[20];
int narg;
// ... (クロージャ変数がなければ早期リターン) ...
// 複合リテラル形式でクロージャを作成
// 例: clos = &struct{F uintptr; A0 *int; A1 *string}{func·001, &i, &s}
typ = nod(OTSTRUCT, N, N); // 構造体型ノード
typ->list = list1(nod(ODCLFIELD, newname(lookup("F")), typenod(types[TUINTPTR]))); // 関数ポインタのフィールド 'F'
for(l=func->cvars; l; l=l->next) {
snprint(buf, sizeof buf, "A%d", narg++);
typ->list = list(typ->list, nod(ODCLFIELD, newname(lookup(buf)), l->n->heapaddr->ntype)); // キャプチャ変数へのポインタフィールド 'A0', 'A1' など
}
clos = nod(OCOMPLIT, N, nod(OIND, typ, N)); // 複合リテラルノード
clos->right->implicit = 1;
clos->list = concat(list1(nod(OCFUNC, func->closure->nname, N)), func->enter); // 関数ポインタとキャプチャ変数の初期化
// *struct から func 型への強制型変換
clos = nod(OCONVNOP, clos, N);
clos->type = func->type;
typecheck(&clos, Erv);
walkexpr(&clos, init);
return clos;
}
src/pkg/runtime/closure_*.c
ファイルの削除:
これらのファイルは、各アーキテクチャ(386, amd64, arm)向けのruntime.closure
関数の実装を含んでいました。この関数は、実行時にクロージャのコードを動的に生成する役割を担っていました。このコミットによって、コンパイル時にクロージャの構造が完全に定義されるようになったため、これらのランタイムコード生成ファイルは不要となり、削除されました。これは、Goのクロージャ実装が、より静的でコンパイル時中心のアプローチに移行したことを明確に示しています。
src/pkg/runtime/asm_arm.s
の変更:
ARMアーキテクチャのアセンブリコードでは、クロージャのコンテキストを保持するレジスタがR0からR7に変更されました。これは、プロローグのコンパイル時にR0が一時レジスタとして使用されることによる競合を避けるためです。
// 変更前 (概念的)
// MOVW 8(FP), R0 // context
// 変更後 (概念的)
MOVW 8(FP), R7 // context
この変更により、ARMアーキテクチャ上でのGoプログラムの安定性とコンパイル時のレジスタ割り当ての効率が向上します。
関連リンク
- Go 1.1 Release Notes: https://go.dev/doc/go1.1 (特に "Language changes" と "Compiler and runtime" のセクション)
- Go issue tracker: https://go.dev/issue/4848 (このコミットに関連する可能性のあるissue)
- Go CL 7369048: https://go.dev/cl/7369048 (このコミットの変更リスト)
参考にした情報源リンク
- https://go.dev/s/go11func (Go 1.1におけるクロージャの変更に関する設計ドキュメント)
- Go言語のクロージャに関する公式ドキュメントやブログ記事 (Go 1.1リリース時の情報)
- Goコンパイラの内部構造に関する資料 (AST、レジスタ割り当てなど)
- ARMアーキテクチャのレジスタ使用規則に関する資料
- ガベージコレクションの仕組みに関する一般的な情報