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

[インデックス 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.cdumpsignatures関数は、オブジェクトファイルに型シグネチャをダンプする役割を担っています。この関数における変更が、重複トランポリン生成の回避に直接寄与します。

変更前は、外部パッケージで定義された*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.cdumpsigt関数は、個々の型シグネチャをダンプする際に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)という条件が追加されました。anilであることは、シグネチャが「空」であることを意味します。つまり、この変更により、DUPOKフラグは空のシグネチャに対してのみ設定されるようになりました。空のシグネチャはコード生成を伴わないため、重複しても問題がないという判断です。

dcl.cexport.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に変更されました。これは、fType型の変数であるため、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.cdumpsignatures 関数

この関数は、コンパイル中の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.hType および 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言語のコンパイラに関する一般的な情報(当時の設計を理解するため)
  • Go言語のインターフェースの内部実装に関する資料
  • リンカとアーカイバ(ar)の動作に関する一般的な情報
  • Go言語の初期のコミット履歴と設計に関する議論
  • Go言語の型システムに関するドキュメント
  • Go言語のコンパイラソースコード内のコメント
  • Go言語のIssue Tracker (当時の関連するバグ報告や議論)
  • Go言語のメーリングリストアーカイブ (当時の開発者間の議論)
  • Go言語のブログ記事や技術解説記事 (コンパイラの内部動作に関するもの)
  • Go言語のsrc/cmd/gc/およびsrc/cmd/6g/ディレクトリ内の他のファイル(コンテキスト理解のため)
  • DUPOKフラグに関する一般的なリンカの知識
  • トランポリンコードに関する一般的なコンパイラの知識
  • Go言語のTypeおよびSym構造体の定義と使用箇所
  • Go言語のコンパイラ開発者Russ Cox氏の過去の発表や論文