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

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

このコミットは、Goコンパイラのcmd/gcにおけるエスケープ解析のバグ修正に関するものです。具体的には、エスケープ解析が変数のエスケープタイプを誤って判断し、重要なEscScopeのエスケープが無視される問題に対処しています。この修正により、コンパイラは変数のライフタイムをより正確に判断し、適切なメモリ割り当て(スタックまたはヒープ)を行うことができるようになります。

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

  • src/cmd/gc/esc.c: エスケープ解析の主要なロジックが含まれるファイル。
  • src/cmd/gc/subr.c: コンパイラのユーティリティ関数が含まれるファイル。
  • test/escape5.go: エスケープ解析のバグを再現し、修正を検証するためのテストファイル。

コミット

commit 71282131a1ab0291834f41e606ebab6c5f0ca438
Author: Russ Cox <rsc@golang.org>
Date:   Wed Nov 7 15:15:21 2012 -0500

    cmd/gc: fix escape analysis bug
    
    The code assumed that the only choices were EscNone, EscScope, and EscHeap,
    so that it makes sense to set EscScope only if the current setting is EscNone.
    Now that we have the many variants of EscReturn, this logic is false, and it was
    causing important EscScopes to be ignored in favor of EscReturn.
    
    Fixes #4360.
    
    R=ken2
    CC=golang-dev, lvd
    https://golang.org/cl/6816103

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

https://github.com/golang/go/commit/71282131a1ab0291834f41e606ebab6c5f0ca438

元コミット内容

commit 71282131a1ab0291834f41e606ebab6c5f0ca438
Author: Russ Cox <rsc@golang.org>
Date:   Wed Nov 7 15:15:21 2012 -0500

    cmd/gc: fix escape analysis bug
    
    The code assumed that the only choices were EscNone, EscScope, and EscHeap,
    so that it makes sense to set EscScope only if the current setting is EscNone.
    Now that we have the many variants of EscReturn, this logic is false, and it was
    causing important EscScopes to be ignored in favor of EscReturn.
    
    Fixes #4360.
    
    R=ken2
    CC=golang-dev, lvd
    https://golang.org/cl/6816103

変更の背景

このコミットは、Goコンパイラのエスケープ解析におけるバグを修正するために行われました。エスケープ解析は、Goプログラムのパフォーマンスを最適化する上で非常に重要な役割を担っています。変数がスタックに割り当てられるか、ヒープに割り当てられるかを決定することで、ガベージコレクションの負荷を軽減し、メモリ効率を向上させます。

問題は、エスケープ解析のコードが、変数のエスケープタイプとしてEscNoneEscScopeEscHeapの3つしか存在しないという誤った前提に基づいて動作していた点にありました。特に、EscScopeを設定する際に、現在のエスケープ設定がEscNoneである場合にのみ設定するというロジックが問題でした。

しかし、Goのエスケープ解析には、関数からの戻り値としてエスケープする様々なケースを示すEscReturnのバリアントが導入されていました。この新しいEscReturnの存在により、上記の間違った前提が崩れ、結果として重要なEscScopeのエスケープがEscReturnによって上書きされ、無視されてしまうというバグが発生していました。これにより、本来スタックに割り当てられるべき変数が不必要にヒープに割り当てられ、パフォーマンスの低下やメモリ使用量の増加につながる可能性がありました。

このバグはIssue #4360として報告されており、このコミットはその問題を解決することを目的としています。

前提知識の解説

Goのエスケープ解析 (Escape Analysis)

Goのエスケープ解析は、コンパイラが行う最適化の一つで、変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかを決定します。

  • スタック割り当て: 関数内で宣言された変数が、その関数の実行中にのみ生存し、関数が終了すると自動的に解放される場合、スタックに割り当てられます。スタック割り当ては高速で、ガベージコレクタの負担を軽減します。
  • ヒープ割り当て: 変数のライフタイムが、それを宣言した関数のスコープを超えて続く場合(例: ポインタが関数から返される、グローバル変数に格納されるなど)、ヒープに割り当てられます。ヒープに割り当てられたメモリはガベージコレクタによって管理されます。

エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、プログラムの効率を高めることです。

エスケープタイプ

Goコンパイラは、変数のエスケープ挙動を分類するためにいくつかのエスケープタイプを使用します。

  • EscNone (Does Not Escape): 変数のライフタイムが、それが宣言された関数内に完全に閉じていることを示します。この場合、変数はスタックに安全に割り当てられます。
  • EscHeap (Escapes to Heap): 変数のライフタイムが、それを宣言した関数のスコープを超えて続く必要があることを示します。この場合、変数はヒープに割り当てられます。これは、ポインタが関数から返される、グローバル変数に格納される、ゴルーチンに渡される、コンパイル時にサイズが決定できない、またはスタックに収まらないほど大きい場合などに発生します。
  • EscReturn (Escapes on Return): EscHeapの特定のケースです。変数のアドレスまたはそのポインタが関数から返されるために、ヒープにエスケープする必要があることを意味します。関数が終了した後もアクセス可能であるためには、ヒープに割り当てられる必要があります。
  • EscScope: これは、EscNoneEscHeapEscReturnのような直接的なエスケープタイプとは少し異なります。エスケープ解析は変数の「動的なスコープ」(実際の生存期間)を決定します。変数の動的なスコープがその字句的なスコープ(宣言された場所)を超えて延長される場合、それは「エスケープ」し、ヒープに割り当てられます。したがって、EscScopeは、変数の使用方法がその直接の関数のスタックフレームよりも長く存続する必要がある状況を暗黙的に指し、結果としてヒープ割り当てにつながります。このコミットの文脈では、EscScopeは特定の変数が特定のスコープ内でエスケープするべきであることを示す内部的なフラグとして機能していると考えられます。

このコミットのバグは、EscReturnのバリアントが導入されたにもかかわらず、エスケープ解析のロジックが古い前提(EscNone, EscScope, EscHeapのみ)に固執していたために発生しました。

技術的詳細

このバグは、src/cmd/gc/esc.c内のescwalk関数におけるエスケープ解析のロジックに起因していました。具体的には、src->esc = EscScope;という行が、src->esc == EscNoneの場合にのみ実行されるという条件が問題でした。

元のコードでは、エスケープタイプがEscNoneである場合にのみ、EscScopeを設定していました。これは、EscNoneEscScopeEscHeapの3つのエスケープタイプしか存在しないという前提に基づいています。この前提では、EscNoneが最も弱いエスケープであり、EscScopeはそれよりも強いエスケープであるため、EscNoneからEscScopeへの昇格は理にかなっていました。

しかし、EscReturnの様々なバリアントが導入されたことで、この前提は誤りとなりました。EscReturnは、変数が関数から戻り値としてエスケープすることを示すものであり、EscNoneよりも強いエスケープです。問題は、ある変数が既にEscReturnとしてマークされている場合でも、その変数がEscScopeとしてマークされるべき状況が存在するにもかかわらず、src->esc == EscNoneの条件のためにEscScopeが設定されなかったことです。

これにより、本来EscScopeとして扱われるべき変数が、EscReturnのまま、あるいは他のエスケープタイプとして誤って扱われ、結果として不適切なヒープ割り当てが発生していました。

修正は以下の2点です。

  1. src/cmd/gc/esc.cescwalk関数内で、EscReturnが設定された後にgoto recurse;が二重に存在していたのを修正し、論理的なフローを改善しました。
  2. src/cmd/gc/esc.cescwalk関数内で、src->esc == EscNoneという条件をsrc->esc != EscHeapに変更しました。これにより、変数が既にEscHeapとしてマークされていない限り、EscScopeを設定できるようになります。EscHeapは最も強いエスケープタイプであり、一度EscHeapと判断された変数は、それ以上弱いエスケープタイプ(EscScopeを含む)に降格されることはありません。この変更により、EscReturnのバリアントが設定されている場合でも、必要に応じてEscScopeが正しく適用されるようになります。

また、src/cmd/gc/subr.cには、デバッグフラグdebug['m']が設定されている場合にエラーをフラッシュする行が追加されています。これは、エスケープ解析のデバッグ出力がより確実に行われるようにするための補助的な変更と考えられます。

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

diff --git a/src/cmd/gc/esc.c b/src/cmd/gc/esc.c
index f789386bc9..f067cc5305 100644
--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -1005,8 +1005,8 @@ escwalk(EscState *e, int level, Node *dst, Node *src)
 			if((src->esc&EscMask) != EscReturn)
 				src->esc = EscReturn;
 			src->esc |= 1<<((dst->vargen-1) + EscBits);
+\t\t\tgoto recurse;
 		}
-\t\t\tgoto recurse;
 	}
 
@@ -1014,7 +1014,7 @@ escwalk(EscState *e, int level, Node *dst, Node *src)
 
 	switch(src->op) {
 	case ONAME:
-\t\tif(src->class == PPARAM && leaks && src->esc == EscNone) {
+\t\tif(src->class == PPARAM && leaks && src->esc != EscHeap) {
 		src->esc = EscScope;
 		if(debug['m'])
 			warnl(src->lineno, "leaking param: %hN", src);
diff --git a/src/cmd/gc/subr.c b/src/cmd/gc/subr.c
index 142921153d..71417bb0a0 100644
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -219,6 +219,8 @@ warnl(int line, char *fmt, ...)
 	va_start(arg, fmt);
 	adderr(line, fmt, arg);
 	va_end(arg);
+\tif(debug['m'])
+\t\tflusherrors();
 }
 
 void
diff --git a/test/escape5.go b/test/escape5.go
index 22c324f902..6b327fe9e3 100644
--- a/test/escape5.go
+++ b/test/escape5.go
@@ -117,3 +117,28 @@ func leakrecursive2(p, q *int) (*int, *int) { // ERROR "leaking param: p" "leaki
 	return p, q
 }
 
+
+var global interface{}
+
+type T1 struct {
+	X *int
+}
+
+type T2 struct {
+	Y *T1
+}
+
+func f8(p *T1) (k T2) { // ERROR "leaking param: p to result k" "leaking param: p"
+	if p == nil {
+		k = T2{}
+		return
+	}
+
+	global = p // should make p leak always
+	return T2{p}
+}
+
+func f9() {
+	var j T1 // ERROR "moved to heap: j"
+	f8(&j) // ERROR "&j escapes to heap"
+}

コアとなるコードの解説

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

  1. goto recurse;の重複修正:

    @@ -1005,8 +1005,8 @@ escwalk(EscState *e, int level, Node *dst, Node *src)
     			if((src->esc&EscMask) != EscReturn)
     				src->esc = EscReturn;
     			src->esc |= 1<<((dst->vargen-1) + EscBits);
    +\t\t\tgoto recurse;
     		}
    -\t\t\tgoto recurse;
     	}
    

    この変更は、EscReturnが設定されるブロック内でgoto recurse;が二重に存在していたのを修正しています。元のコードでは、ifブロックの内部と外部の両方にgoto recurse;があり、冗長でした。この修正により、ifブロックの内部にのみgoto recurse;が残され、コードの可読性と論理的なフローが改善されました。これは直接的なバグ修正というよりは、コードのクリーンアップと正確性の向上を目的としています。

  2. EscScope設定条件の変更:

    @@ -1014,7 +1014,7 @@ escwalk(EscState *e, int level, Node *dst, Node *src)
     
     	switch(src->op) {
     	case ONAME:
    -\t\tif(src->class == PPARAM && leaks && src->esc == EscNone) {
    +\t\tif(src->class == PPARAM && leaks && src->esc != EscHeap) {
     		src->esc = EscScope;
     		if(debug['m'])
     			warnl(src->lineno, "leaking param: %hN", src);
    

    これがこのコミットの主要なバグ修正箇所です。

    • 変更前: src->esc == EscNone
      • これは、「変数のエスケープタイプが現在EscNoneである場合にのみ、EscScopeを設定する」という意味でした。
      • 前述の通り、EscReturnのバリアントが導入されたことで、この条件は不十分になりました。変数が既にEscReturnとしてマークされている場合でも、EscScopeとしてマークされるべき状況が存在するにもかかわらず、この条件のためにEscScopeが設定されませんでした。
    • 変更後: src->esc != EscHeap
      • これは、「変数のエスケープタイプが現在EscHeapではない場合に、EscScopeを設定する」という意味です。
      • EscHeapは最も強いエスケープタイプであり、一度EscHeapと判断された変数は、それ以上弱いエスケープタイプに降格されることはありません。
      • この変更により、変数がEscNoneであるか、あるいはEscReturnのいずれかのバリアントである場合でも、EscHeapでない限りEscScopeが正しく適用されるようになります。これにより、エスケープ解析が変数のライフタイムをより正確に判断し、適切なメモリ割り当てを行うことができるようになります。

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

diff --git a/src/cmd/gc/subr.c b/src/cmd/gc/subr.c
index 142921153d..71417bb0a0 100644
--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -219,6 +219,8 @@ warnl(int line, char *fmt, ...)
 	va_start(arg, fmt);
 	adderr(line, fmt, arg);
 	va_end(arg);
+\tif(debug['m'])
+\t\tflusherrors();
 }

warnl関数(警告メッセージを出力する関数)に、debug['m']フラグが設定されている場合にflusherrors()を呼び出す行が追加されました。

  • debug['m']は、Goコンパイラのエスケープ解析のデバッグ出力を有効にするためのフラグです(go build -gcflags="-m")。
  • flusherrors()は、おそらくバッファリングされたエラーメッセージを強制的にフラッシュする関数です。
  • この変更は、エスケープ解析のデバッグ出力がより確実に行われるようにするための補助的なものであり、バグ修正の直接的な原因ではありませんが、デバッグ時の情報提供を改善します。

test/escape5.goの変更点

このファイルには、新しいテストケースf8f9が追加されています。これらのテストケースは、EscScopeが正しく適用されるべき状況で、以前のバグによって誤ったエスケープ解析が行われていたケースを再現し、修正が正しく機能することを確認するために使用されます。

  • f8関数は、p *T1というパラメータを受け取り、T2型の戻り値を返します。global = pという行により、pがグローバル変数にエスケープし、ヒープに割り当てられるべきであることを示します。また、戻り値kpが含まれるため、pが戻り値としてエスケープする(EscReturn)と同時に、そのスコープも考慮されるべき状況を作り出しています。
  • f9関数は、f8を呼び出し、ローカル変数jのアドレスを渡しています。このテストは、jがヒープに移動し、&jがヒープにエスケープすることを検証しています。

これらのテストケースは、特にEscReturnEscScopeの相互作用が正しく処理されることを保証するために重要です。

関連リンク

参考にした情報源リンク