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

[インデックス 14765] ファイルの概要

このコミットは、Goコンパイラ(cmd/gc)において、不要なvar _ = ...形式の変数宣言に対するコード生成を抑制する変更を導入しています。具体的には、ブランク識別子(_)への代入で、かつその右辺の式が副作用を持たない(破棄可能である)場合に、コンパイラがその代入に対するコードを生成しないように最適化を行います。これにより、コンパイルされたバイナリのサイズ削減と、コンパイル時間の短縮に寄与します。

コミット

commit 6592456feb7a9f934e82f2fde1ef2b395eaa44f8
Author: Russ Cox <rsc@golang.org>
Date:   Sun Dec 30 12:01:53 2012 -0500

    cmd/gc: do not generate code for var _ = ... unless necessary
    
    Fixes #2443.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6997048
---
 src/cmd/gc/go.h    |   1 +
 src/cmd/gc/init.c  |   4 ++\n src/cmd/gc/sinit.c |   7 ++++\n src/cmd/gc/walk.c  | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++-\n test/golden.out    |   6 +--\n test/sinit.go      |  10 +++++
 6 files changed, 133 insertions(+), 6 deletions(-)

diff --git a/src/cmd/gc/go.h b/src/cmd/gc/go.h
index 071422367c..accb19cd99 100644
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -1375,6 +1375,7 @@ void	walkexprlistsafe(NodeList *l, NodeList **init);
 void	walkstmt(Node **np);
 void	walkstmtlist(NodeList *l);
 Node*	conv(Node*, Type*);
+int	candiscard(Node*);
 
 /*
  *	arch-specific ggen.c/gsubr.c/gobj.c/pgen.c
diff --git a/src/cmd/gc/init.c b/src/cmd/gc/init/c
index be402cc0ce..918d37180b 100644
--- a/src/cmd/gc/init.c
+++ b/src/cmd/gc/init.c
@@ -55,6 +55,10 @@ anyinit(NodeList *n)
 		case ODCLTYPE:
 		case OEMPTY:
 			break;
+		case OAS:
+			if(isblank(l->n->left) && candiscard(l->n->right))
+				break;
+			// fall through
 		default:
 			return 1;
 		}
diff --git a/src/cmd/gc/sinit.c b/src/cmd/gc/sinit.c
index e8010099d2..e1a0758da1 100644
--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -108,6 +108,13 @@ init1(Node *n, NodeList **out)
 		case OAS:
 			if(n->defn->left != n)
 				goto bad;
+			if(isblank(n->defn->left) && candiscard(n->defn->right)) {
+				n->defn->op = OEMPTY;
+				n->defn->left = N;
+				n->defn->right = N;
+				break;
+			}
+
 		/*
 			n->defn->dodata = 1;
 			init1(n->defn->right, out);
diff --git a/src/cmd/gc/walk.c b/src/cmd/gc/walk.c
index 98b2a4fa74..3a2152b092 100644
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -183,8 +183,8 @@ walkstmt(Node **np)
 		dump("nottop", n);
 		break;
 
-	case OASOP:
 	case OAS:
+	case OASOP:
 	case OAS2:
 	case OAS2DOTTYPE:
 	case OAS2RECV:
@@ -3226,3 +3226,112 @@ usefield(Node *n)
 	curfn->paramfld = l;
 }
 
+static int
+candiscardlist(NodeList *l)
+{
+	for(; l; l=l->next)
+		if(!candiscard(l->n))
+			return 0;
+	return 1;
+}
+
+int
+candiscard(Node *n)
+{
+	if(n == N)
+		return 1;
+	
+	switch(n->op) {
+	default:
+		return 0;
+
+	case ONAME:
+	case ONONAME:
+	case OTYPE:
+	case OPACK:
+	case OLITERAL:
+	case OADD:
+	case OSUB:
+	case OOR:
+	case OXOR:
+	case OADDSTR:
+	case OADDR:
+	case OANDAND:
+	case OARRAYBYTESTR:
+	case OARRAYRUNESTR:
+	case OSTRARRAYBYTE:
+	case OSTRARRAYRUNE:
+	case OCAP:
+	case OCMPIFACE:
+	case OCMPSTR:
+	case OCOMPLIT:
+	case OMAPLIT:
+	case OSTRUCTLIT:
+	case OARRAYLIT:
+	case OPTRLIT:
+	case OCONV:
+	case OCONVIFACE:
+	case OCONVNOP:
+	case ODOT:
+	case OEQ:
+	case ONE:
+	case OLT:
+	case OLE:
+	case OGT:
+	case OGE:
+	case OKEY:
+	case OLEN:
+	case OMUL:
+	case OLSH:
+	case ORSH:
+	case OAND:
+	case OANDNOT:
+	case ONEW:
+	case ONOT:
+	case OCOM:
+	case OPLUS:
+	case OMINUS:
+	case OOROR:
+	case OPAREN:
+	case ORUNESTR:
+	case OREAL:
+	case OIMAG:
+	case OCOMPLEX:
+		// Discardable as long as the subpieces are.
+		break;
+
+	case ODIV:
+	case OMOD:
+		// Discardable as long as we know it's not division by zero.
+		if(isconst(n->right, CTINT) && mpcmpfixc(n->right->val.u.xval, 0) != 0)
+			break;
+		if(isconst(n->right, CTFLT) && mpcmpfltc(n->right->val.u.fval, 0) != 0)
+			break;
+		return 0;
+
+	case OMAKECHAN:
+	case OMAKEMAP:
+		// Discardable as long as we know it won't fail because of a bad size.
+		if(isconst(n->left, CTINT) && mpcmpfixc(n->left->val.u.xval, 0) == 0)
+			break;
+		return 0;
+	
+	case OMAKESLICE:
+		// Difficult to tell what sizes are okay.
+		return 0;		
+	}
+	
+	if(!candiscard(n->left) ||
+	   !candiscard(n->right) ||
+	   !candiscard(n->ntest) ||
+	   !candiscard(n->nincr) ||
+	   !candiscardlist(n->ninit) ||
+	   !candiscardlist(n->nbody) ||
+	   !candiscardlist(n->nelse) ||
+	   !candiscardlist(n->list) ||
+	   !candiscardlist(n->rlist)) {
+		return 0;
+	}
+	
+	return 1;
+}
diff --git a/test/golden.out b/test/golden.out
index 3e44e04c6a..742a5d3f63 100644
--- a/test/golden.out
+++ b/test/golden.out
@@ -16,13 +16,9 @@
 == fixedbugs/
 
 =========== fixedbugs/bug429.go
-throw: all goroutines are asleep - deadlock!
+fatal error: all goroutines are asleep - deadlock!
 
 == bugs/
 
 =========== bugs/bug395.go
 bug395 is broken
--
-=========== bugs/bug434.go
-bugs/bug434.dir/two.go:10: one.t.int undefined (cannot refer to unexported field or method one.int)
-BUG:bug434
diff --git a/test/sinit.go b/test/sinit.go
index ffb8ef7511..5e50e1100a 100644
--- a/test/sinit.go
+++ b/test/sinit.go
@@ -259,3 +259,13 @@ var copy_pt0a = pt0a
 var copy_pt0b = pt0b
 var copy_pt1 = pt1
 var copy_pt1a = pt1a
+\
+var _ interface{} = 1
+\
+type T1 int
+\
+func (t *T1) M() {}\
+\
+type Mer interface { M() }\
+\
+var _ Mer = (*T1)(nil)\n

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/6592456feb7a9f934e82f2fde1ef2b395eaa44f8

元コミット内容

このコミットの元の内容は、「cmd/gc: do not generate code for var _ = ... unless necessary」と題されており、Goコンパイラ(cmd/gc)が、必要でない限りvar _ = ...形式の宣言に対してコードを生成しないようにするというものです。これは、Go言語のブランク識別子(_)を用いた代入文が、特定の状況下で不要なコードを生成してしまう問題を解決することを目的としています。コミットメッセージには「Fixes #2443」とあり、GoのIssue 2443を修正するものであることが示唆されていますが、現在のGoのIssueトラッカーでは直接的なIssue 2443は見つかりませんでした。これは、古いIssue番号であるか、または内部的なトラッキング番号である可能性があります。また、「https://golang.org/cl/6997048」というGerritの変更リストへのリンクも含まれていますが、このChange-IDは現在、別のコミット(cmd/go: add -C flag to 'go run')に関連付けられています。これは、GerritのChange-IDが再利用されることがあるためか、または参照が古い情報である可能性があります。

変更の背景

Go言語では、ブランク識別子_は、変数を宣言したがその値を使用しない場合や、関数の戻り値の一部を破棄したい場合などに利用されます。例えば、インポートしたパッケージのinit関数を実行したいだけで、そのパッケージ内の識別子を直接使用しない場合などにimport _ "package/path"のように使われます。同様に、var _ = expressionという形式は、expressionが評価されることを保証しつつ、その結果を破棄したい場合によく用いられます。これは、特定の関数が副作用を持つことを確認したり、コンパイル時に型チェックを行わせたりする目的で使われることがあります。

しかし、var _ = expressionという形式の代入が、expressionが純粋な(副作用のない)値である場合、その代入自体はプログラムの動作に影響を与えません。このような場合でも、コンパイラが常にコードを生成してしまうと、生成されるバイナリのサイズが増加し、コンパイル時間も無駄に長くなる可能性があります。

このコミットの背景には、このような不要なコード生成を排除し、コンパイラの効率を向上させるという目的があります。特に、var _ = ...が型アサーションやインターフェースの実装チェックのために使われる場合、実行時にそのコードが不要であれば、コンパイル時に最適化して取り除くことが望ましいとされます。

前提知識の解説

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラの一つで、Goソースコードを機械語に変換する役割を担います。gcは「Go Compiler」の略です。Goのツールチェインの一部として提供され、Goプログラムのビルド時に自動的に呼び出されます。
  • ブランク識別子 (_): Go言語の特殊な識別子で、値を破棄するために使用されます。変数宣言で_を使用すると、その変数に代入された値は使用されないことを明示します。例えば、_, err := someFunc()のように、エラーだけを処理したい場合などに使われます。
  • var _ = ...: この構文は、主に以下の目的で使用されます。
    • 副作用の強制: ...の部分に副作用を持つ式(例: fmt.Println("hello"))を記述し、その副作用が確実に実行されることを保証します。
    • 型チェック/インターフェース実装の確認: 特定の型がインターフェースを実装していることをコンパイル時に確認するために使用されます。例えば、var _ io.Reader = (*MyStruct)(nil)と書くことで、MyStructio.Readerインターフェースを実装しているかをコンパイル時にチェックできます。この場合、(*MyStruct)(nil)MyStruct型のnilポインタであり、実行時には何もしません。
  • コード生成: コンパイラがソースコードを解析し、実行可能な機械語コードや中間コードを生成するプロセスです。最適化は、このコード生成の段階で行われ、より効率的で高速なコードを生成することを目指します。
  • 副作用 (Side Effect): プログラムの実行中に、関数の戻り値以外に、プログラムの状態(変数、ファイル、ネットワークなど)を変更する操作のことです。例えば、fmt.Printlnは標準出力に文字列を出力するという副作用を持ちます。純粋な関数は副作用を持ちません。
  • AST (Abstract Syntax Tree): ソースコードの構文構造を抽象的に表現したツリー構造です。コンパイラはソースコードをASTに変換し、そのASTを操作して最適化やコード生成を行います。Goコンパイラも内部でASTを扱います。

技術的詳細

このコミットの核心は、candiscardという新しい関数の導入と、既存のコンパイラパスでのその利用です。

candiscard(Node *n)関数は、与えられたASTノードnが表す式が、副作用を持たずに破棄可能であるかどうかを判断します。つまり、その式が評価されてもプログラムの状態に影響を与えない場合、candiscardは真(1)を返します。

candiscard関数は、様々な種類のASTノード(n->op)に対して、以下のように振る舞います。

  • 純粋な操作: ONAME, OLITERAL, OADD, OSUB, OCONVなど、多くの基本的な演算やリテラル、型変換などは、そのオペランドが破棄可能であれば、自身も破棄可能と判断されます。これらの操作は、それ自体が副作用を持たないためです。
  • 条件付きで破棄可能な操作:
    • ODIV, OMOD (除算、剰余): これらの操作は、ゼロ除算の可能性があるため、右辺が定数でゼロでない場合にのみ破棄可能と判断されます。ゼロ除算はパニックを引き起こす副作用とみなされるため、これを避ける必要があります。
    • OMAKECHAN, OMAKEMAP (チャネル、マップの作成): これらの操作は、サイズが定数でゼロの場合にのみ破棄可能と判断されます。サイズがゼロでない場合、メモリ割り当てなどの副作用が発生する可能性があります。
  • 破棄不可能な操作: OMAKESLICE (スライスの作成) は、そのサイズや容量の計算が複雑であり、常に副作用がないとは限らないため、破棄不可能とされています。また、defaultケースでは、明示的に破棄可能とされていない全ての操作は破棄不可能と判断されます。
  • 再帰的なチェック: candiscardは、ノードの子ノード(n->left, n->right, n->ntest, n->nincrなど)や、関連するノードリスト(n->ninit, n->nbody, n->nelse, n->list, n->rlist)に対しても再帰的にcandiscardまたはcandiscardlistを呼び出し、全てが破棄可能である場合にのみ、そのノード全体が破棄可能であると判断します。

このcandiscard関数が導入された後、Goコンパイラの初期化処理(init.csinit.c)およびASTのウォーク処理(walk.c)において、var _ = expression形式の代入文が最適化の対象となります。

具体的には、isblank(l->n->left)(左辺がブランク識別子であるか)とcandiscard(l->n->right)(右辺が副作用を持たずに破棄可能であるか)の両方が真である場合、その代入文はOEMPTY(空の操作)に変換されます。OEMPTYはコンパイラによってコードが生成されないため、結果として不要な代入に対する機械語コードが生成されなくなります。

コアとなるコードの変更箇所

このコミットで変更された主要なファイルと関数は以下の通りです。

  • src/cmd/gc/go.h: candiscard関数のプロトタイプ宣言が追加されました。
  • src/cmd/gc/init.c: anyinit関数内で、OAS(代入)操作がブランク識別子への代入であり、かつ右辺がcandiscardである場合に、初期化リストからその代入をスキップするロジックが追加されました。
  • src/cmd/gc/sinit.c: init1関数内で、OAS操作がブランク識別子への代入であり、かつ右辺がcandiscardである場合に、その代入ノードのオペレーションをOEMPTYに設定し、左右の子ノードをN(nil)にすることで、コード生成を抑制するロジックが追加されました。
  • src/cmd/gc/walk.c:
    • candiscardlist関数が追加されました。これはNodeList内の全てのノードがcandiscardであるかをチェックします。
    • candiscard関数が実装されました。この関数が、様々なASTノードのタイプに基づいて、その式が副作用を持たずに破棄可能であるかを判断する主要なロジックを含んでいます。
    • walkstmt関数内のOAS(代入)のcase文の順序が変更されましたが、これは機能的な変更ではなく、OASOP(複合代入)とOASの処理順序を調整したものです。
  • test/golden.out: テストの出力結果が更新されました。これは、コンパイラの変更によって特定のテストケースの出力が変わったことを示しています。
  • test/sinit.go: 新しいテストケースが追加されました。特に、var _ interface{} = 1var _ Mer = (*T1)(nil)のような、ブランク識別子への代入を含むテストケースが追加され、このコミットの変更が正しく機能することを確認しています。

コアとなるコードの解説

src/cmd/gc/go.h

+int	candiscard(Node*);

candiscard関数の前方宣言が追加されました。これにより、他のファイルからこの関数を呼び出すことが可能になります。

src/cmd/gc/init.c

@@ -55,6 +55,10 @@ anyinit(NodeList *n)
 		case ODCLTYPE:
 		case OEMPTY:
 			break;
+		case OAS:
+			if(isblank(l->n->left) && candiscard(l->n->right))
+				break;
+			// fall through
 		default:
 			return 1;
 		}

anyinit関数は、与えられたノードリストに初期化が必要な操作が含まれているかをチェックします。ここで、OAS(代入)の場合に、左辺がブランク識別子(isblank(l->n->left))であり、かつ右辺が破棄可能(candiscard(l->n->right))であれば、その代入は初期化の必要がないと判断され、breakで処理をスキップします。これにより、不要な初期化処理が回避されます。

src/cmd/gc/sinit.c

@@ -108,6 +108,13 @@ init1(Node *n, NodeList **out)
 		case OAS:
 			if(n->defn->left != n)
 				goto bad;
+			if(isblank(n->defn->left) && candiscard(n->defn->right)) {
+				n->defn->op = OEMPTY;
+				n->defn->left = N;
+				n->defn->right = N;
+				break;
+			}
+
 		/*
 			n->defn->dodata = 1;
 			init1(n->defn->right, out);

init1関数は、初期化が必要なノードを処理します。ここでもOAS(代入)の場合に、左辺がブランク識別子であり、かつ右辺が破棄可能であれば、そのノードのオペレーションをOEMPTYに書き換えます。OEMPTYはコンパイラがコードを生成しない特殊なオペレーションであり、これにより不要な代入に対するコード生成が完全に抑制されます。また、子ノードもN(nil)に設定することで、それ以上の処理が行われないようにします。

src/cmd/gc/walk.c

+static int
+candiscardlist(NodeList *l)
+{
+	for(; l; l=l->next)
+		if(!candiscard(l->n))
+			return 0;
+	return 1;
+}
+
+int
+candiscard(Node *n)
+{
+	if(n == N)
+		return 1;
+	
+	switch(n->op) {
+	default:
+		return 0;
+
+	case ONAME:
+	case ONONAME:
+	case OTYPE:
+	case OPACK:
+	case OLITERAL:
+	case OADD:
+	// ... (中略) ...
+	case OCOMPLEX:
+		// Discardable as long as the subpieces are.
+		break;
+
+	case ODIV:
+	case OMOD:
+		// Discardable as long as we know it's not division by zero.
+		if(isconst(n->right, CTINT) && mpcmpfixc(n->right->val.u.xval, 0) != 0)
+			break;
+		if(isconst(n->right, CTFLT) && mpcmpfltc(n->right->val.u.fval, 0) != 0)
+			break;
+		return 0;
+
+	case OMAKECHAN:
+	case OMAKEMAP:
+		// Discardable as long as we know it won't fail because of a bad size.
+		if(isconst(n->left, CTINT) && mpcmpfixc(n->left->val.u.xval, 0) == 0)
+			break;
+		return 0;
+	
+	case OMAKESLICE:
+		// Difficult to tell what sizes are okay.
+		return 0;		
+	}
+	
+	if(!candiscard(n->left) ||
+	   !candiscard(n->right) ||
+	   !candiscard(n->ntest) ||
+	   !candiscard(n->nincr) ||
+	   !candiscardlist(n->ninit) ||
+	   !candiscardlist(n->nbody) ||
+	   !candiscardlist(n->nelse) ||
+	   !candiscardlist(n->list) ||
+	   !candiscardlist(n->rlist)) {
+		return 0;
+	}
+	
+	return 1;
+}

candiscardlistは、NodeList内の全てのノードが破棄可能であるかを再帰的にチェックするヘルパー関数です。

candiscard関数は、このコミットの主要なロジックを担います。

  • n == N(ノードがnil)の場合は破棄可能とします。
  • switch(n->op)文で、様々なASTノードのタイプを処理します。
    • 多くの純粋な演算子(OADD, OSUBなど)やリテラル(OLITERAL)は、その子ノードが破棄可能であれば自身も破棄可能と判断されます。
    • ODIV, OMODは、ゼロ除算の可能性を考慮し、右辺が定数でゼロでない場合にのみ破棄可能とします。
    • OMAKECHAN, OMAKEMAPは、左辺(サイズ)が定数でゼロの場合にのみ破棄可能とします。
    • OMAKESLICEは、常に破棄不可能とします。
    • defaultケースでは、明示的に破棄可能とされていない全てのノードは破棄不可能とします。
  • 最後に、ノードの全ての子ノード(n->left, n->rightなど)や関連するノードリスト(n->ninit, n->nbodyなど)が再帰的にcandiscardまたはcandiscardlistによって破棄可能であるかをチェックします。これら全てが破棄可能であれば、そのノード全体が破棄可能であると判断し、1を返します。そうでなければ0を返します。

関連リンク

  • GitHubコミットページ: https://github.com/golang/go/commit/6592456feb7a9f934e82f2fde1ef2b395eaa44f8
  • Go Issue #2443: コミットメッセージに記載されていますが、現在のGoのIssueトラッカーでは直接的なIssue 2443は見つかりませんでした。
  • Gerrit Change-ID golang.org/cl/6997048: コミットメッセージに記載されていますが、現在のGerritではこのChange-IDは別のコミット(cmd/go: add -C flag to 'go run')に関連付けられています。

参考にした情報源リンク

  • 上記のGitHubコミットページ
  • Go言語の公式ドキュメント(Goコンパイラ、ブランク識別子、ASTに関する一般的な情報)