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

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

このコミットは、Goコンパイラのガベージコレクション(GC)におけるエスケープ解析、インライン化、およびクロージャに関連するバグを修正するものです。具体的には、コンパイラのバックエンド部分であるsrc/cmd/gcディレクトリ内のファイルが変更されています。また、この修正を検証するための新しいテストケースが追加されています。

変更されたファイルは以下の通りです。

  • src/cmd/gc/esc.c: エスケープ解析のロジックを実装しているC言語のソースファイル。
  • src/cmd/gc/go.h: Goコンパイラの共通ヘッダーファイル。
  • src/cmd/gc/lex.c: 字句解析およびコンパイルの主要なフローを制御するC言語のソースファイル。
  • test/escape2.go: 既存のエスケープ解析テストファイル。コメントの修正のみ。
  • test/escape4.go: 新規追加されたエスケープ解析のテストケース。

コミット

commit 075eef4018b1c2ab37c9236e3265f0d2d816a04f
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 23 23:09:53 2012 -0500

    gc: fix escape analysis + inlining + closure bug

    R=ken2
    CC=golang-dev, lvd
    https://golang.org/cl/5693056

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

https://github.com/golang/go/commit/075eef4018b1c2ab37c9236e3265f0d2d816a04f

元コミット内容

gc: fix escape analysis + inlining + closure bug

R=ken2
CC=golang-dev, lvd
https://golang.org/cl/5693056

変更の背景

このコミットは、Goコンパイラのエスケープ解析における特定のバグを修正するために行われました。このバグは、関数がインライン化されたり、クロージャ(匿名関数)が使用されたりする際に、変数のメモリ割り当てに関する誤った判断を引き起こす可能性がありました。具体的には、スタックに割り当てられるべき変数が誤ってヒープに割り当てられたり、その逆のケースが発生したりすることで、パフォーマンスの低下や、まれに不正な動作につながる可能性がありました。

特に、クロージャのコンパイル中に新しく生成されるコード(インライン化された関数など)に対して、エスケープ解析が適切に実行されないことが問題でした。これにより、クロージャ内で使用される変数が、本来はスタックに割り当てられるべきであるにもかかわらず、ヒープに割り当てられてしまうという非効率なコードが生成されることがありました。test/escape4.goのコメント「// Escape analysis used to miss inlined code in closures.」がこの問題を明確に示しています。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの概念と最適化技術について理解しておく必要があります。

  1. エスケープ解析 (Escape Analysis): エスケープ解析は、コンパイラ最適化の一種で、変数がその宣言されたスコープを「エスケープ」するかどうかを決定します。

    • スタック割り当て: 関数内で宣言された変数がその関数の実行中にのみ必要とされ、関数が終了すると不要になる場合、その変数は通常、高速なスタックメモリに割り当てられます。スタックはLIFO(後入れ先出し)構造で、メモリの割り当てと解放が非常に効率的です。
    • ヒープ割り当て: 変数がその宣言されたスコープを超えて参照される可能性がある場合(例:ポインタが関数から返される、グローバル変数に代入される、クロージャによってキャプチャされるなど)、その変数はヒープメモリに割り当てられます。ヒープはより柔軟なメモリ管理を提供しますが、ガベージコレクション(GC)の対象となり、スタックに比べて割り当てと解放のオーバーヘッドが大きくなります。 エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、GCの負荷を減らし、プログラムのパフォーマンスを向上させることです。
  2. インライン化 (Inlining): インライン化は、コンパイラ最適化の一種で、呼び出し元の関数に呼び出される関数のコードを直接埋め込むプロセスです。これにより、関数呼び出しのオーバーヘッド(スタックフレームのセットアップ、引数の渡し、戻り値の処理など)が削減され、プログラムの実行速度が向上します。また、インライン化によって、コンパイラは呼び出し元と呼び出される関数の両方のコンテキストを考慮した、より高度な最適化(エスケープ解析を含む)を実行できるようになります。

  3. クロージャ (Closures): クロージャは、関数がその定義された環境(レキシカルスコープ)を記憶し、その環境内の変数にアクセスできる機能を持つ関数です。Goでは、匿名関数がクロージャとして機能することがよくあります。クロージャが外部スコープの変数を参照する場合、その変数はクロージャの生存期間中アクセス可能である必要があるため、ヒープに割り当てられることがよくあります。しかし、クロージャがすぐに実行され、キャプチャした変数がその実行後すぐに不要になる場合など、エスケープ解析によってスタックに割り当てられるべきケースも存在します。

このコミットのバグは、これらの概念が複雑に絡み合う状況、特にインライン化されたコードがクロージャ内で使用される場合に、エスケープ解析が正しく機能しないというものでした。

技術的詳細

このコミットが修正する問題は、Goコンパイラのコンパイルフェーズにおけるエスケープ解析の実行タイミングとスコープに関するものです。

Goコンパイラのコンパイルフローは、いくつかのフェーズに分かれています。

  1. フェーズ5: エスケープ解析: トップレベルの関数に対してエスケープ解析が実行されます。
  2. フェーズ6: トップレベル関数のコンパイル: トップレベルの関数がコンパイルされます。このフェーズでインライン化が行われる可能性があります。
  3. フェーズ6b: クロージャのコンパイル: クロージャがコンパイルされます。このフェーズはループで実行され、クロージャのコンパイル中にさらに新しいクロージャが生成される可能性があるため、繰り返し処理されます。

問題は、フェーズ6bでクロージャがコンパイルされる際に、そのクロージャ内でインライン化されたコードに対して、エスケープ解析が再度実行されていなかった点にありました。エスケープ解析は、コードの構造や変数の使われ方に基づいてメモリ割り当てを決定するため、インライン化によってコードの構造が変化した場合、その変化を考慮して再度解析を行う必要があります。

このコミットの修正は、src/cmd/gc/lex.c内のクロージャコンパイルループに、新しく生成されたクロージャのバッチに対してエスケープ解析を再実行するステップを追加することで、この問題を解決しています。

具体的には、以下の変更が行われました。

  1. escapes関数の引数化 (src/cmd/gc/esc.c, src/cmd/gc/go.h):

    • これまで引数なしで呼び出されていたescapes関数が、NodeList *allという引数を取るように変更されました。これにより、エスケープ解析の対象となる関数のリストを外部から指定できるようになりました。
    • 以前はxtop(トップレベルの関数リスト)を直接参照していましたが、この変更により、特定の関数のサブセット(例:新しく生成されたクロージャのバッチ)に対してエスケープ解析を実行することが可能になりました。
  2. クロージャコンパイルループ内のエスケープ解析の追加 (src/cmd/gc/lex.c):

    • main関数内のクロージャコンパイルループ(while(closures))が修正されました。
    • 以前は、クロージャのバッチに対してインライン化(inlcalls)と関数コンパイル(funccompile)のみが実行されていました。
    • 修正後、インライン化の後に、現在のクロージャのバッチ(batch)に対してescapes(batch)が呼び出されるようになりました。これにより、クロージャ内でインライン化されたコードを含む、新しく生成されたすべてのクロージャに対して、適切なエスケープ解析が実行されるようになります。

この変更により、コンパイラはクロージャとインライン化の組み合わせによって生じる複雑なケースでも、変数のエスケープ挙動を正確に判断し、最適なメモリ割り当て(スタックまたはヒープ)を行うことができるようになりました。結果として、生成されるバイナリの効率が向上し、ガベージコレクションのオーバーヘッドが削減されます。

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

src/cmd/gc/esc.c

--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -59,7 +59,7 @@ static int	dstcount, edgecount;	// diagnostic
 static NodeList*	noesc;	// list of possible non-escaping nodes, for printing
 
 void
-escapes(void)
+escapes(NodeList *all)
 {
 	NodeList *l;
 
@@ -70,9 +70,10 @@ escapes(void)
 	theSink.escloopdepth = -1;
 
 	safetag = strlit("noescape");
+	noesc = nil;
 
-	// flow-analyze top level functions
-	for(l=xtop; l; l=l->next)
+	// flow-analyze functions
+	for(l=all; l; l=l->next)
 		if(l->n->op == ODCLFUNC || l->n->op == OCLOSURE)
 			escfunc(l->n);
 
@@ -84,7 +85,7 @@ escapes(void)
 		escflood(l->n);
 
 	// for all top level functions, tag the typenodes corresponding to the param nodes
-	for(l=xtop; l; l=l->next)
+	for(l=all; l; l=l->next)
 		if(l->n->op == ODCLFUNC)
 			esctag(l->n);

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -955,7 +955,7 @@ NodeList*	variter(NodeList *vl, Node *t, NodeList *el);\n /*
  *\tesc.c
  */
-void	escapes(void);\n+void	escapes(NodeList*);\n \n /*
  *\texport.c

src/cmd/gc/lex.c

--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -390,7 +390,7 @@ int
 main(int argc, char *argv[])
 {
 	int i, c;
-	NodeList *l;
+	NodeList *l, *batch;
 	char *p;
 
 #ifdef	SIGBUS
@@ -401,14 +401,17 @@ main(int argc, char *argv[])
 
 	// Phase 5: escape analysis.
 	if(!debug['N'])
-		escapes();
+		escapes(xtop);
 
 	// Phase 6: Compile top level functions.
 	for(l=xtop; l; l=l->next)
 		funccompile(l->n, 0);
 	if(debug['l'])
 		fninit(xtop);
 
 	// Phase 6b: Compile all closures.
+	// Can generate more closures, so run in batches.
 	while(closures) {
-		l = closures;
+		batch = closures;
 		closures = nil;
-		for(; l; l=l->next) {
-			if (debug['l'])
+		if(debug['l'])
+			for(l=batch; l; l=l->next)
 				inlcalls(l->n);
+		if(!debug['N'])
+			escapes(batch);
+		for(l=batch; l; l=l->next)
 			funccompile(l->n, 1);
-		}
 	}
 
 	// Phase 7: check external declarations.

test/escape4.go (新規ファイル)

// errchk -0 $G -m $D/$F.go

// Copyright 2010 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.

// Test, using compiler diagnostic flags, that the escape analysis is working.
// Compiles but does not run.  Inlining is enabled.

package foo

var p *int

func alloc(x int) *int {  // ERROR "can inline alloc" "moved to heap: x"
	return &x  // ERROR "&x escapes to heap"
}

var f func()

func f1() {
	p = alloc(2) // ERROR "inlining call to alloc" "&x escapes to heap" "moved to heap: x"

	// Escape analysis used to miss inlined code in closures.

	func() {  // ERROR "func literal does not escape"
		p = alloc(3)  // ERROR "inlining call to alloc" "&x escapes to heap" "moved to heap: x"
	}()
	
	f = func() {  // ERROR "func literal escapes to heap"
		p = alloc(3)  // ERROR "inlining call to alloc" "&x escapes to heap" "moved to heap: x"
	}
	f()
}

コアとなるコードの解説

src/cmd/gc/esc.csrc/cmd/gc/go.h の変更

  • escapes関数のシグネチャがvoid escapes(void)からvoid escapes(NodeList *all)に変更されました。
  • これにより、エスケープ解析の対象となるノードのリストを引数として渡せるようになり、特定の関数群(例えば、新しく生成されたクロージャのバッチ)に対してのみエスケープ解析を実行する柔軟性が生まれました。
  • 関数内のループもxtop(トップレベル関数)ではなく、引数allで渡されたリストを処理するように変更されています。
  • noesc = nil; の追加は、escapes関数が呼び出されるたびにnoescリストが初期化されることを保証し、以前の実行からの状態が残らないようにします。

src/cmd/gc/lex.c の変更

このファイルでの変更が、バグ修正の核心部分です。

  • main関数内の初期のエスケープ解析呼び出しがescapes()からescapes(xtop)に変更されました。これは、トップレベルの関数に対しては引き続きエスケープ解析が実行されることを意味します。
  • 最も重要な変更は、クロージャのコンパイルループ(while(closures))内です。
    • 以前は、クロージャのバッチに対してインライン化(inlcalls)と関数コンパイル(funccompile)が実行されるだけでした。
    • 修正後、インライン化の後に、if(!debug['N']) escapes(batch);という行が追加されました。
    • この行は、デバッグフラグ'N'(エスケープ解析を無効にするフラグ)が設定されていない限り、現在処理中のクロージャのバッチ(batch)に対してエスケープ解析を再実行します。
    • これにより、クロージャのコンパイル中にインライン化によって生成された新しいコードや、クロージャ自体がキャプチャする変数などに対して、エスケープ解析が適切に適用されるようになります。

test/escape4.go の追加

  • この新しいテストファイルは、修正されたバグを具体的に検証するために作成されました。
  • alloc関数は、ローカル変数xのアドレスを返すため、xはヒープにエスケープする必要があります。
  • このテストは、alloc関数が直接呼び出される場合と、匿名関数(クロージャ)内で呼び出される場合の両方で、エスケープ解析が正しく機能するかどうかを検証します。
  • 特に、クロージャ内でallocがインライン化されるケースがテストされており、// Escape analysis used to miss inlined code in closures.というコメントが、このテストが修正対象のバグを狙っていることを明確に示しています。
  • ERRORコメントは、コンパイラが期待するエスケープ解析の診断メッセージ(例:「&x escapes to heap」、「moved to heap: x」、「inlining call to alloc」)を示しており、テストが成功するためにはこれらのメッセージが出力される必要があります。

これらの変更により、Goコンパイラは、インライン化とクロージャが組み合わさった複雑なシナリオにおいても、変数のエスケープ挙動を正確に判断し、より効率的なコードを生成できるようになりました。

関連リンク

  • Go CL 5693056: https://golang.org/cl/5693056 注: Web検索ではCL 569356が関連付けられることがありますが、コミットメッセージに記載されているのはCL 5693056です。このコミットは、Goコンパイラの内部的な修正であり、特定のCVEとは直接関連しない可能性があります。
  • Go Issue #39511 (Escape analysis on closures): https://github.com/golang/go/issues/39511 このIssueは、クロージャにおけるエスケープ解析の一般的な問題について議論しており、このコミットが修正した問題と関連している可能性があります。

参考にした情報源リンク

  • GitHubコミットページ: https://github.com/golang/go/commit/075eef4018b1c2ab37c9236e3265f0d2d816a04f
  • Go言語のエスケープ解析に関する一般的な情報源(例: Go公式ブログ、Go言語のコンパイラに関するドキュメントなど) 具体的なURLはコミット情報には含まれていませんが、エスケープ解析、インライン化、クロージャの概念はGo言語のコンパイラ最適化の基本的な部分であり、公式ドキュメントや関連する技術記事で詳細に解説されています。