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

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

このコミットは、Goコンパイラのインライン化処理において、インライン化された関数内のラベルが重複する問題を修正するものです。具体的には、goto文やラベルが使用されている関数が複数回インライン化される際に、生成されるラベル名が衝突しないように、一意な識別子を付加する変更が加えられました。これにより、issue #4748で報告されたコンパイルエラーが解決されます。

コミット

commit 09a17ca1f113b7959391b0daf49ecfcd930cf30b
Author: Russ Cox <rsc@golang.org>
Date:   Sun Feb 3 11:19:22 2013 -0500

    cmd/gc: make inlined labels distinct
    
    Fixes #4748.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/7261044

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

https://github.com/golang/go/commit/09a17ca1f113b7959391b0daf49ecfcd930cf30b

元コミット内容

cmd/gc: make inlined labels distinct

Fixes #4748.

R=ken2
CC=golang-dev
https://golang.org/cl/7261044

変更の背景

この変更は、Goコンパイラにおけるバグ issue #4748 を修正するために行われました。このバグは、goto文とラベルを含む関数が複数回インライン化される場合に発生しました。

Goコンパイラは、パフォーマンス向上のために、小さな関数呼び出しを呼び出し元に直接展開する「インライン化」という最適化を行います。しかし、インライン化される関数内にgoto文と対応するラベル(例: exit:)が存在し、その関数がコード内で複数回呼び出され、結果として複数回インライン化されると問題が発生しました。

具体的には、コンパイラがインライン化されたコードを生成する際、同じラベル名(例: exit)が複数回定義されてしまい、コンパイラが「重複したラベル定義」としてエラーを報告していました。これは、インライン化によって関数の本体がコピーされる際に、その内部のラベル名が一意に保たれないために起こる問題でした。

このコミットは、このラベル名の衝突を回避し、インライン化が正しく行われるようにするために導入されました。

前提知識の解説

Goコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、意味解析、中間コード生成、最適化、コード生成などが含まれます。

インライン化 (Inlining)

インライン化は、コンパイラ最適化の一種です。関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を削減するために、呼び出される関数の本体を呼び出し元のコードに直接埋め込む(インライン展開する)技術です。これにより、実行時のパフォーマンスが向上する可能性があります。Goコンパイラは、特定の条件(関数が小さい、呼び出し回数が多いなど)を満たす関数に対して自動的にインライン化を適用します。

ラベルとgoto

Go言語には、他の多くのプログラミング言語と同様に、goto文とラベルが存在します。goto文は、プログラムの実行フローを、指定されたラベルの位置に直接ジャンプさせるために使用されます。ラベルは、labelName:のように定義され、goto labelNameで参照されます。

func example() {
    // ...
    goto end
    // ...
end:
    // ...
}

goto文は、コードの可読性を損なう可能性があるため、Goでは通常、forループのbreakcontinueswitch文など、より構造化された制御フローを使用することが推奨されます。しかし、特定の状況(例えば、エラーハンドリングやリソースのクリーンアップなど)でgotoが有効な場合もあります。

抽象構文木 (AST) とノード

コンパイラは、ソースコードを直接操作するのではなく、それを抽象構文木(AST)というツリー構造に変換して処理します。ASTの各要素は「ノード」と呼ばれ、変数、関数呼び出し、演算子、制御構造(ifforgotoなど)などを表現します。コンパイラの最適化やコード生成は、このASTを走査・変換することで行われます。

技術的詳細

このコミットの核心は、インライン化されたコード内でラベル名が衝突する問題を解決することです。Goコンパイラのインライン化処理は、src/cmd/gc/inl.cファイルで実装されています。

問題は、inlsubst関数がASTノードを走査し、インライン化のためにノードを置換する際に発生しました。特に、OGOTOgoto文)とOLABEL(ラベル定義)のノードが処理される際に、元のラベル名がそのままコピーされてしまうため、同じ関数が複数回インライン化されると、同じ名前のラベルが複数生成されてしまうのです。

この修正では、この問題を解決するために以下のメカニズムが導入されました。

  1. インライン化世代カウンター (inlgen) の導入: static int inlgen; という静的変数が src/cmd/gc/inl.c に追加されました。このカウンターは、関数がインライン化されるたびにインクリメントされます。これにより、各インライン化操作に一意の「世代」番号が割り当てられます。

  2. ラベル名の動的生成: inlsubst関数内で、OGOTOおよびOLABELノードが処理される際に、新しいラベル名が動的に生成されるようになりました。 smprint("%s·%d", n->left->sym->name, inlgen) という形式の文字列フォーマットが使用されます。

    • n->left->sym->name: 元のラベル名(例: exit
    • inlgen: 現在のインライン化世代カウンターの値

    これにより、例えば exit というラベルは、最初のインライン化では exit·1、2回目のインライン化では exit·2 のように、一意な名前に変更されます。· (中点) は、Goの内部リンカが使用する特殊な区切り文字であり、ユーザーが定義する識別子と衝突しないように設計されています。

  3. 新しいシンボルの作成: 生成された新しいラベル名(例: exit·1)は、lookup(p) を通じて新しいシンボルとして登録され、newname で新しい名前ノードが作成されます。これにより、コンパイラは各インライン化されたラベルを別々のエンティティとして認識し、名前の衝突を回避できます。

この変更により、goto文とラベルを含む関数が何度インライン化されても、それぞれのインスタンスでラベル名が一意になり、コンパイルエラーが発生しなくなりました。

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

src/cmd/gc/inl.c

--- a/src/cmd/gc/inl.c
+++ b/src/cmd/gc/inl.c
@@ -510,6 +510,8 @@ tinlvar(Type *t)
 	return nblank;
 }
 
+static int inlgen;
+
 // if *np is a call, and fn is a function with an inlinable body, substitute *np with an OINLCALL.
 // On return ninit has the parameter assignments, the nbody is the
 // inlined function body and list, rlist contain the input, output
@@ -730,6 +732,7 @@ mkinlcall1(Node **np, Node *fn, int isddd)
 	}
 
 	inlretlabel = newlabel();
+	inlgen++;
 	body = inlsubstlist(fn->inl);
 
 	body = list(body, nod(OGOTO, inlretlabel, N));	// avoid 'not used' when function doesnt have return
@@ -855,6 +858,7 @@ inlsubstlist(NodeList *ll)
 static Node*
 inlsubst(Node *n)
 {
+	char *p;
 	Node *m, *as;
 	NodeList *ll;
 
@@ -897,6 +901,16 @@ inlsubst(Node *n)
 		typecheck(&m, Etop);
 //		dump("Return after substitution", m);
 		return m;
+	
+	case OGOTO:
+	case OLABEL:
+		m = nod(OXXX, N, N);
+		*m = *n;
+		m->ninit = nil;
+		p = smprint("%s·%d", n->left->sym->name, inlgen);	
+		m->left = newname(lookup(p));
+		free(p);
+		return m;	
 	}
 
 

test/fixedbugs/issue4748.go

--- /dev/null
+++ b/test/fixedbugs/issue4748.go
@@ -0,0 +1,20 @@
+// run
+
+// Copyright 2013 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Issue 4748.
+// This program used to complain because inlining created two exit labels.
+
+package main
+
+func jump() {
+        goto exit
+exit:
+        return
+}
+func main() {
+        jump()
+        jump()
+}

コアとなるコードの解説

src/cmd/gc/inl.c の変更点

  1. static int inlgen; の追加: これは、インライン化の「世代」を追跡するための静的カウンターです。コンパイラ全体で共有され、インライン化が実行されるたびにその値が更新されます。

  2. inlgen++; の追加 (mkinlcall1 関数内): mkinlcall1 関数は、実際にインライン化を行う主要な関数の一つです。この関数が呼び出され、関数がインライン化される直前に inlgen がインクリメントされます。これにより、各インライン化操作が異なる inlgen の値を持つことになり、生成されるラベル名の一意性が保証されます。

  3. inlsubst 関数内の case OGOTO:case OLABEL: の追加: inlsubst 関数は、インライン化される関数のASTを走査し、必要に応じてノードを置換する役割を担います。

    • OGOTOOLABEL のケースが追加されました。これは、goto文とラベル定義のノードを特別に処理することを示します。
    • m = nod(OXXX, N, N); *m = *n; m->ninit = nil;: これは、元のノード n のコピー m を作成し、初期化リストをクリアする標準的なパターンです。
    • p = smprint("%s·%d", n->left->sym->name, inlgen);: ここが最も重要な変更点です。smprint は、Goコンパイラ内部で使用される文字列フォーマット関数です。元のラベル名 (n->left->sym->name) と現在の inlgen の値を組み合わせて、新しい一意なラベル名(例: exit·1)を生成します。· (中点) は、Goの内部リンカが使用する特殊な文字で、ユーザーが定義する識別子と衝突しないように設計されています。
    • m->left = newname(lookup(p));: 生成された新しいラベル名 p を使用して、lookup でシンボルテーブルから対応するシンボルを検索(または新規作成)し、newname で新しい名前ノードを作成して m->left に割り当てます。これにより、AST内のラベル参照が新しい一意な名前に更新されます。
    • free(p);: smprint で割り当てられたメモリを解放します。

test/fixedbugs/issue4748.go の変更点

このファイルは、issue #4748 で報告されたバグを再現し、このコミットによって修正されたことを検証するための新しいテストケースです。

  • func jump(): この関数は、goto exitexit: ラベルを含んでいます。この関数がインライン化の対象となります。
  • func main(): main 関数内で jump() が2回呼び出されています。この2回の呼び出しがインライン化されると、修正前は exit: ラベルが重複して定義されることになり、コンパイルエラーが発生していました。

このテストケースは、// run ディレクティブによって、Goのテストスイートがこのファイルをコンパイルして実行することを指示しています。このコミットが適用された後、このテストはエラーなくコンパイル・実行されるようになり、バグが修正されたことを確認できます。

関連リンク

参考にした情報源リンク

  • Go Issue 4748 の詳細
  • Goコンパイラのインライン化に関する一般的な情報
  • Go言語のgoto文とラベルに関するドキュメント
  • Goコンパイラのソースコード (src/cmd/gc/inl.c) の分析