[インデックス 1323] ファイルの概要
このコミットは、Goコンパイラ(特に6g
およびgc
)における型シグネチャの生成ロジックを改善し、重複するトランポリンコードの生成を防ぐことを目的としています。具体的には、非自明な(non-trivial)型シグネチャが、その型が定義されたファイル内でのみ生成されるように変更することで、複数のコンパイル単位間で同じトランポリンが生成される問題を回避します。これにより、リンカエラーやバイナリサイズの増大といった問題を抑制します。
コミット
commit cb64ec5bb6c35f66b1262b5dc2a36840b456a353
Author: Russ Cox <rsc@golang.org>
Date: Thu Dec 11 11:54:33 2008 -0800
only generate non-trivial signatures in the
file in which they occur. avoids duplicate
trampoline generation across multiple files.
R=ken
OCL=20976
CL=20980
---
src/cmd/6g/obj.c | 23 ++++++++++++++---------
src/cmd/gc/dcl.c | 13 +++++++------
src/cmd/gc/export.c | 2 +-\
src/cmd/gc/go.h | 2 +-\
src/cmd/gc/walk.c | 3 ++-\
5 files changed, 25 insertions(+), 18 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cb64ec5bb6c35f66b1262b5dc2a36840b456a353
元コミット内容
only generate non-trivial signatures in the
file in which they occur. avoids duplicate
trampoline generation across multiple files.
R=ken
OCL=20976
CL=20980
変更の背景
Go言語の初期のコンパイラ(gc
および各アーキテクチャ固有のコンパイラ、例: 6g
はx86-64向け)では、型情報、特にインターフェース型や構造体型に関する「シグネチャ」をオブジェクトファイルに埋め込んでいました。これらのシグネチャは、実行時に型アサーションやインターフェースメソッド呼び出しを効率的に行うためのメタデータや、場合によっては「トランポリン」と呼ばれる小さなコードスニペットを生成するために使用されます。
問題は、同じ型が複数のソースファイルで参照される場合、その型のシグネチャや関連するトランポリンコードが、それぞれのオブジェクトファイルに重複して生成される可能性があったことです。リンカは通常、重複するシンボルを解決しますが、特定の種類の重複(特にコードセクション内の重複)は問題を引き起こす可能性があります。具体的には、ar
(アーカイバ)が重複するトランポリンの扱いを適切に行えない場合があり、結果としてリンカエラーや予期せぬ動作につながる可能性がありました。
このコミットは、この重複生成の問題に対処するために導入されました。解決策は、非自明なシグネチャ(つまり、コード生成を伴う可能性のあるシグネチャ)を、その型が実際に定義されているソースファイル内でのみ生成するように制限することです。これにより、他のファイルではその型のシグネチャが生成されなくなり、重複が回避されます。
前提知識の解説
Goコンパイラの構造 (gc, 6g)
gc
(Go Compiler): Go言語のフロントエンドコンパイラ。ソースコードの字句解析、構文解析、型チェック、中間表現への変換、最適化など、言語に依存しない大部分の処理を担当します。6g
(Go Compiler for x86-64):gc
によって生成された中間表現を受け取り、特定のアーキテクチャ(この場合はx86-64)向けの機械語コードを生成するバックエンドコンパイラです。8g
はx86、5g
はARMなど、他のアーキテクチャにも同様のバックエンドが存在しました。- オブジェクトファイル (
.6
など): 各ソースファイルがコンパイルされると、対応するオブジェクトファイルが生成されます。これには、コンパイルされたコード、データ、そして型情報などのメタデータが含まれます。
型システムと型シグネチャ
Go言語は静的型付け言語であり、コンパイル時に厳密な型チェックが行われます。
「シグネチャ」とは、ここではGoの型に関するメタデータを指します。特に、インターフェース型や構造体型が持つメソッドセットの情報などが含まれます。これらの情報は、実行時の型アサーション(例: x.(T)
)やインターフェースメソッドの動的ディスパッチ(どの具体的なメソッドを呼び出すか)に必要となります。
シンボル (Sym
) と型 (Type
) の内部表現
Goコンパイラの内部では、プログラムの要素は様々なデータ構造で表現されます。
Sym
(Symbol): 変数名、関数名、型名などの識別子(シンボル)を表すデータ構造です。シンボルは、その識別子がどのパッケージに属し、どのような種類(変数、関数、型など)であるかといった情報を含みます。Type
(Type): Go言語の型(int
,string
,struct
,interface
など)を表すデータ構造です。型の構造、基底型、メソッドセットなどの情報を含みます。
これらの構造体には、コンパイルプロセス中に様々なフラグやメタデータが追加されます。このコミットでは、特にlocal
というフラグが重要になります。
トランポリン (Trampoline) の概念
コンピュータサイエンスにおいて「トランポリン」とは、ある関数から別の関数へジャンプするための中間的な小さなコードスニペットを指します。Go言語のコンテキスト、特にインターフェースの動的ディスパッチにおいては、インターフェース型の値が保持する具体的な型のメソッドを呼び出す際に、適切なメソッドの実装へジャンプするためのコードが生成されることがあります。これが「トランポリン」と呼ばれることがあります。
例えば、io.Reader
インターフェースのRead
メソッドを呼び出す際、具体的な型が*bytes.Buffer
であれば(*bytes.Buffer).Read
へ、*os.File
であれば(*os.File).Read
へ、それぞれ異なるアドレスへジャンプする必要があります。このジャンプを仲介するコードがトランポリンとして機能します。
DUPOK
フラグの役割
DUPOK
は "Duplicate OK" の略で、オブジェクトファイル内のシンボルに付与されるフラグの一つです。このフラグが設定されたシンボルは、複数のオブジェクトファイルに同じ名前で存在してもリンカがエラーとせず、そのうちの一つを選択してリンクすることを許可します。これは、例えばC++のテンプレートインスタンス化などで、同じコードが複数のコンパイル単位で生成される場合に利用されることがあります。
このコミットでは、DUPOK
が「空のシグネチャ」に対してのみ適用されるように変更されています。これは、空のシグネチャはコード生成を伴わないため、重複しても問題がないという判断に基づいています。
コンパイル単位とリンケージ
Goのコンパイルは、通常、パッケージ単位で行われます。各ソースファイルは個別にコンパイルされ、オブジェクトファイルが生成されます。その後、これらのオブジェクトファイルがリンカによって結合され、最終的な実行可能ファイルやライブラリが作成されます。このリンケージの過程で、異なるオブジェクトファイル間でシンボルが解決されます。
技術的詳細
このコミットの核心は、型シグネチャの生成を、その型が「ローカルに定義されている」ファイルに限定することです。ここでいう「ローカルに定義されている」とは、その型が現在のコンパイル単位(ソースファイル)内で宣言されたものであることを意味します。
local
フラグの移動と意味合い
最も重要な変更点の一つは、local
フラグがSym
構造体からType
構造体へ移動したことです。
- 変更前:
Sym
構造体にuchar local; // created in this file
というフィールドがありました。これは、シンボル(識別子)が現在のファイルで定義されたものかどうかを示していました。 - 変更後:
Sym
構造体からlocal
フィールドが削除され、代わりにType
構造体にuchar local; // created in this file
というフィールドが追加されました。
この変更は、Goコンパイラの内部設計における「ローカル性」の概念が、シンボルレベルから型レベルへと移行したことを示唆しています。つまり、ある型が現在のファイルで定義されたものかどうかを、その型のシンボルではなく、型そのものの属性として管理するようになったということです。これは、型シグネチャの生成ロジックが型そのものに強く依存するため、より自然な設計変更と言えます。
dumpsignatures
関数での変更点
src/cmd/6g/obj.c
のdumpsignatures
関数は、オブジェクトファイルに型シグネチャをダンプする役割を担っています。この関数における変更が、重複トランポリン生成の回避に直接寄与します。
変更前は、外部パッケージで定義された*NamedStruct
やインターフェースのシグネチャを最適化としてスキップしていましたが、変更後はより厳密に「このファイル外で定義された型については、非自明なシグネチャをエミットしない」というロジックが導入されました。
--- a/src/cmd/6g/obj.c
+++ b/src/cmd/6g/obj.c
@@ -891,17 +893,20 @@ dumpsignatures(void)
s->siggen = 1;
-//print("dosig %T\n", t);
- // don't emit signatures for *NamedStruct or interface if
- // they were defined by other packages.
- // (optimization)
+ // don't emit non-trivial signatures for types defined outside this file.
+ // non-trivial signatures might also drag in generated trampolines,
+ // and ar can't handle duplicates of the trampolines.
s1 = S;
- if(isptr[et] && t->type != T)
+ if(isptr[et] && t->type != T) {
s1 = t->type->sym;
- else if(et == TINTER)
+ if(s1 && !t->type->local)
+ continue;
+ }
+ else if(et == TINTER) {
s1 = t->sym;
- if(s1 != S && strcmp(s1->opackage, package) != 0)
- continue;
+ if(s1 && !t->local)
+ continue;
+ }
if(et == TINTER)
dumpsigi(t, s);
この変更により、以下の条件でシグネチャの生成がスキップされます。
- ポインタ型(
isptr[et]
)の場合、その基底型(t->type
)がローカルでない(!t->type->local
)場合。 - インターフェース型(
et == TINTER
)の場合、そのインターフェース型自体がローカルでない(!t->local
)場合。
これにより、外部パッケージや他のファイルで定義された型については、そのシグネチャ(特にトランポリンを伴う可能性のある非自明なもの)が現在のオブジェクトファイルに重複して生成されることがなくなります。
dumpsigt
関数での DUPOK
の適用条件の変更
src/cmd/6g/obj.c
のdumpsigt
関数は、個々の型シグネチャをダンプする際にDUPOK
フラグを設定するロジックを含んでいます。
--- a/src/cmd/6g/obj.c
+++ b/src/cmd/6g/obj.c
@@ -707,10 +707,12 @@ dumpsigt(Type *t0, Sym *s)\n
// set DUPOK to allow other .6s to contain
// the same signature. only one will be chosen.
+\t// should only happen for empty signatures
tp = pc;\n gins(AGLOBL, N, N);\n p->from = at;\n-\tp->from.scale = DUPOK;\n+\tif(a == nil)\n+\t\tp->from.scale = DUPOK;\n p->to = ac;\n p->to.offset = ot;\n }
変更前は常にp->from.scale = DUPOK;
を設定していましたが、変更後はif(a == nil)
という条件が追加されました。a
がnil
であることは、シグネチャが「空」であることを意味します。つまり、この変更により、DUPOK
フラグは空のシグネチャに対してのみ設定されるようになりました。空のシグネチャはコード生成を伴わないため、重複しても問題がないという判断です。
dcl.c
と export.c
での local
フラグの参照箇所の修正
local
フラグがSym
からType
へ移動したことに伴い、このフラグを参照していた他のファイルも修正されています。
src/cmd/gc/dcl.c
: 宣言処理を行うファイル。dodcltype
: 型を宣言する際に、n->sym->local = 1;
からn->local = 1;
に変更され、型自体にlocal
フラグが設定されるようになりました。updatetype
: 型を更新する際に、if(n->local) t->local = 1;
が追加され、更新元の型がローカルであれば、更新先の型もローカルとしてマークされるようになりました。addmethod
: メソッドを追加する際に、レシーバ型がローカルであるかどうかのチェックが!st->local
から!f->local
に変更されました。これは、f
がType
型の変数であるため、Type
構造体のlocal
フィールドを参照するように修正されたものです。dcopy
: シンボルをコピーする際に、a->local = b->local;
の行が削除されました。これは、local
フラグがSym
から削除されたためです。
src/cmd/gc/export.c
: 型をエクスポートする際に、dumptype
関数で!t->sym->local
から!t->local
に変更され、型自体のlocal
フラグを参照するようになりました。
これらの変更は、local
フラグのセマンティクスがシンボルから型へと移行したことに対する、コンパイラコードベース全体での整合性維持のための修正です。
コアとなるコードの変更箇所
src/cmd/6g/obj.c
(シグネチャ生成ロジックの変更)
--- a/src/cmd/6g/obj.c
+++ b/src/cmd/6g/obj.c
@@ -891,17 +893,20 @@ dumpsignatures(void)
s->siggen = 1;
-//print("dosig %T\n", t);
- // don't emit signatures for *NamedStruct or interface if
- // they were defined by other packages.
- // (optimization)
+ // don't emit non-trivial signatures for types defined outside this file.
+ // non-trivial signatures might also drag in generated trampolines,
+ // and ar can't handle duplicates of the trampolines.
s1 = S;
- if(isptr[et] && t->type != T)
+ if(isptr[et] && t->type != T) {
s1 = t->type->sym;
- else if(et == TINTER)
+ if(s1 && !t->type->local)
+ continue;
+ }
+ else if(et == TINTER) {
s1 = t->sym;
- if(s1 != S && strcmp(s1->opackage, package) != 0)
- continue;
+ if(s1 && !t->local)
+ continue;
+ }
if(et == TINTER)
dumpsigi(t, s);
src/cmd/gc/go.h
(local
フラグの移動)
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -157,6 +157,7 @@ struct Type
uchar siggen;
uchar funarg;
uchar copyany;
+\tuchar local; // created in this file
// TFUNCT
uchar thistuple;
@@ -238,7 +239,6 @@ struct Sym
uchar exported; // exported
uchar imported; // imported
uchar sym; // huffman encoding in object file
-\tuchar local; // created in this file
uchar uniq; // imbedded field name first found
uchar siggen; // signature generated
コアとなるコードの解説
src/cmd/6g/obj.c
の dumpsignatures
関数
この関数は、コンパイル中のGoソースファイルから生成されるオブジェクトファイルに、Goの型に関する「シグネチャ」情報を書き出す役割を担っています。シグネチャには、型の構造やメソッドセットなどのメタデータが含まれ、Goのランタイムが型アサーションやインターフェースメソッドの動的ディスパッチを行うために利用します。
変更の目的は、**「非自明なシグネチャ(特にトランポリンコードを伴う可能性のあるもの)は、その型が定義されたファイル内でのみ生成する」**というポリシーを強制することです。
-
変更前のコメントアウトされた行:
// don't emit signatures for *NamedStruct or interface if // they were defined by other packages. // (optimization)
これは、以前の最適化の試みを示しています。外部パッケージで定義された型についてはシグネチャの生成をスキップしていましたが、このロジックは不十分だったか、あるいは「非自明なシグネチャ」というより厳密な条件をカバーしていなかった可能性があります。
-
新しいコメント:
// don't emit non-trivial signatures for types defined outside this file. // non-trivial signatures might also drag in generated trampolines, // and ar can't handle duplicates of the trampolines.
このコメントは、変更の具体的な理由と目的を明確にしています。「非自明なシグネチャ」が問題であり、それが「生成されたトランポリン」を引き込む可能性があり、
ar
(アーカイバ)がその重複を扱えないため、この制限が必要であると説明しています。 -
新しい条件分岐:
if(isptr[et] && t->type != T) { s1 = t->type->sym; if(s1 && !t->type->local) continue; } else if(et == TINTER) { s1 = t->sym; if(s1 && !t->local) continue; }
このコードブロックが、新しいシグネチャ生成の制限を実装しています。
isptr[et] && t->type != T
: 現在処理している型t
がポインタ型であり、かつその基底型が存在する場合。s1 = t->type->sym;
: 基底型のシンボルを取得します。if(s1 && !t->type->local) continue;
: もし基底型のシンボルが存在し、かつその基底型が現在のファイルで定義されたものでない(!t->type->local
)ならば、このシグネチャの生成をスキップします。
et == TINTER
: 現在処理している型t
がインターフェース型の場合。s1 = t->sym;
: インターフェース型自身のシンボルを取得します。if(s1 && !t->local) continue;
: もしインターフェース型のシンボルが存在し、かつそのインターフェース型が現在のファイルで定義されたものでない(!t->local
)ならば、このシグネチャの生成をスキップします。
このロジックにより、ポインタの指す型やインターフェース型が、現在のコンパイル単位(ファイル)で定義されていない場合、その型のシグネチャは生成されなくなります。これにより、他のファイルで既に生成されている可能性のある重複するトランポリンコードの生成が効果的に防止されます。
src/cmd/gc/go.h
の Type
および Sym
構造体
このヘッダーファイルは、Goコンパイラの内部で使用される主要なデータ構造であるType
(型)とSym
(シンボル)の定義を含んでいます。
-
Type
構造体へのlocal
フィールドの追加:struct Type { // ... 既存のフィールド ... uchar local; // created in this file // ... 既存のフィールド ... };
uchar local;
がType
構造体に追加されました。このフィールドは、そのType
インスタンスが現在のコンパイル単位(ソースファイル)内で定義されたものであるかどうかを示すフラグとして機能します。uchar
は通常、1バイトの符号なし整数であり、ここではブーリアン値(0または1)として使用されます。 -
Sym
構造体からのlocal
フィールドの削除:struct Sym { // ... 既存のフィールド ... // uchar local; // created in this file (削除された行) // ... 既存のフィールド ... };
Sym
構造体からlocal
フィールドが削除されました。これは、local
という概念の「所有者」がシンボルから型へと変更されたことを意味します。以前はシンボルが「このファイルで作成されたか」を保持していましたが、このコミット以降は型がその情報を保持するようになりました。この変更は、型シグネチャの生成ロジックが型そのものに強く依存するため、よりセマンティックな整合性を保つためのリファクタリングと言えます。
これらの変更は、コンパイラの内部モデルにおいて、型の「ローカル性」をより正確に表現し、それに基づいてシグネチャ生成の振る舞いを制御するための基盤を確立しています。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Goコンパイラのソースコード(現在のバージョン): https://github.com/golang/go/tree/master/src/cmd/compile (当時の
6g
やgc
は現在のcmd/compile
に統合されています)
参考にした情報源リンク
- Go言語のコンパイラに関する一般的な情報(当時の設計を理解するため)
- Go言語のインターフェースの内部実装に関する資料
- リンカとアーカイバ(
ar
)の動作に関する一般的な情報 - Go言語の初期のコミット履歴と設計に関する議論
- Go言語の型システムに関するドキュメント
- Go言語のコンパイラソースコード内のコメント
- Go言語のIssue Tracker (当時の関連するバグ報告や議論)
- Go言語のメーリングリストアーカイブ (当時の開発者間の議論)
- Go言語のブログ記事や技術解説記事 (コンパイラの内部動作に関するもの)
- Go言語の
src/cmd/gc/
およびsrc/cmd/6g/
ディレクトリ内の他のファイル(コンテキスト理解のため) DUPOK
フラグに関する一般的なリンカの知識- トランポリンコードに関する一般的なコンパイラの知識
- Go言語の
Type
およびSym
構造体の定義と使用箇所 - Go言語のコンパイラ開発者Russ Cox氏の過去の発表や論文