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

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

このコミットは、Goコンパイラのガベージコレクション(gc)ツールにおけるエスケープ解析の改善に関するものです。特に、クロージャに対するエスケープ解析の結果を適用することで、特定のシナリオでのメモリ割り当てを削減し、パフォーマンスを向上させています。

コミット

commit 9fe60801aeca801010b42e1dd7ad57a173dc9740
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Feb 26 00:40:28 2013 +0100

    cmd/gc: apply escape analysis results to closures.
    
    This avoids an allocation when closures are used
    as "macros", in Walk idioms, or as argument to defer.
    
    benchmark                old ns/op    new ns/op    delta
    BenchmarkSearchWrappers       1171          354  -69.77%
    BenchmarkCallClosure             3            3  -12.54%
    BenchmarkCallClosure1          119            7  -93.95%
    BenchmarkCallClosure2          183           74  -59.18%
    BenchmarkCallClosure3          187           75  -59.57%
    BenchmarkCallClosure4          187           76  -58.98%
    
    Compared to Go 1:
    benchmark                  old ns/op    new ns/op    delta
    BenchmarkSearchWrappers         3208          354  -88.97%
    
    Fixes #3520.
    
    R=daniel.morsing, bradfitz, minux.ma, dave, rsc
    CC=golang-dev
    https://golang.org/cl/7397056

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

https://github.com/golang/go/commit/9fe60801aeca801010b42e1dd7ad57a173dc9740

元コミット内容

Goコンパイラ(cmd/gc)において、クロージャに対するエスケープ解析の結果を適用するように変更しました。これにより、クロージャが「マクロ」として、Walkイディオムで、またはdeferの引数として使用される場合に、不要なメモリ割り当てを回避します。

この変更により、ベンチマークで顕著なパフォーマンス改善が見られました。特にBenchmarkSearchWrappersでは約70%の高速化、BenchmarkCallClosure1では90%以上の高速化を達成しています。Go 1と比較しても、BenchmarkSearchWrappersで約89%の高速化が確認されています。

このコミットは、Issue #3520を修正するものです。

変更の背景

Go言語では、クロージャは非常に強力な機能であり、コードの簡潔性や表現力を高めるために広く利用されます。しかし、クロージャがキャプチャする変数は、そのクロージャがスコープ外で実行される可能性があるため、ヒープに割り当てられる(エスケープする)必要があります。これは、ガベージコレクションのオーバーヘッドを増加させ、パフォーマンスに影響を与える可能性があります。

このコミットの背景には、Goコンパイラのエスケープ解析がクロージャに対して十分に最適化されていなかったという問題がありました。特に、クロージャが一時的な目的で、例えばループ内で「マクロ」のように使われたり、defer文の引数として使われたりする場合でも、不必要にヒープ割り当てが発生していました。これは、コンパイラがクロージャの生存期間を正確に判断できず、安全側に倒してヒープに割り当てていたためです。

Issue #3520は、まさにこの問題、すなわちクロージャが不必要にヒープに割り当てられることによるパフォーマンスの低下を指摘していました。このコミットは、その問題を解決し、Goプログラムの実行効率を向上させることを目的としています。

前提知識の解説

エスケープ解析 (Escape Analysis)

エスケープ解析は、コンパイラ最適化の一種で、プログラム内の変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定するプロセスです。

  • スタック割り当て: 関数内で宣言されたローカル変数は、通常、その関数が実行されている間だけ有効なメモリ領域であるスタックに割り当てられます。関数が終了すると、スタックフレームが破棄され、その変数が占めていたメモリは自動的に解放されます。これは非常に高速で、ガベージコレクションのオーバーヘッドがありません。
  • ヒープ割り当て: 変数がその宣言されたスコープ(関数)の外でも参照され続ける可能性がある場合(例えば、ポインタが関数から返される場合や、グローバル変数に代入される場合など)、その変数はヒープに割り当てられます。ヒープはプログラム全体で共有されるメモリ領域であり、ガベージコレクタによって管理されます。ヒープ割り当てはスタック割り当てよりもコストが高く、ガベージコレクションの対象となるため、パフォーマンスに影響を与える可能性があります。

エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、ヒープ割り当ての回数を減らし、ガベージコレクションの負荷を軽減し、プログラムの実行速度を向上させることです。

クロージャ (Closures)

クロージャは、関数と、その関数が宣言された環境(レキシカルスコープ)を組み合わせたものです。クロージャは、その定義時に存在していた非ローカル変数(自由変数)を「キャプチャ」し、その変数をクロージャが実行される際にも利用できるようにします。

Go言語において、クロージャがキャプチャする変数は、そのクロージャが宣言された関数が終了した後も生存し続ける必要があるため、通常はヒープに割り当てられます。しかし、クロージャがその宣言された関数内でのみ使用され、その関数の終了とともに寿命が尽きるような場合、ヒープ割り当ては不要であり、スタックに割り当てることが可能です。

Walk イディオム

Goコンパイラ(cmd/gc)の内部では、AST(抽象構文木)を走査(Walk)する際に、特定の処理を適用するためにクロージャが頻繁に利用されます。これは、ツリー構造を再帰的に探索し、各ノードでカスタムのロジックを実行する一般的なパターンです。このような内部的な処理で生成されるクロージャが、不必要にヒープに割り当てられると、コンパイラ自身のパフォーマンスにも影響を与えます。

defer

defer文は、その関数がリターンする直前、またはパニックが発生した場合に実行される関数呼び出しをスケジュールします。deferに渡される関数は、多くの場合、クロージャです。例えば、リソースの解放(ファイルのクローズ、ロックの解除など)によく使われます。deferに渡されるクロージャが不必要にヒープに割り当てられると、関数の終了時に余分なガベージコレクションのオーバーヘッドが発生する可能性があります。

技術的詳細

このコミットの技術的詳細の中心は、Goコンパイラのcmd/gcにおけるクロージャの生成とエスケープ解析の連携を改善することです。

Goコンパイラは、ソースコードをASTに変換し、そのASTに対して様々な最適化パスを実行します。エスケープ解析もその一つです。クロージャがコード内で定義されると、コンパイラはそれを内部的に表現する構造を生成します。この構造は、クロージャのコード自体と、キャプチャされた変数への参照を含みます。

変更前は、クロージャが生成される際に、そのクロージャがヒープにエスケープするかどうかの判断が、クロージャの生成ロジックとエスケープ解析の結果との間で十分に連携していませんでした。特に、walkclosure関数(src/cmd/gc/closure.c内)は、クロージャを表すノード(OCOMPLIT)を生成し、そのノードを型チェック(typecheck)に渡します。型チェックの後、さらにwalkexprが呼び出されます。

このコミットの変更は、walkclosure関数内で、クロージャノード(clos)に、そのクロージャが属する関数(func)のエスケープ解析結果(func->esc)を伝播させるようにしています。具体的には、以下の2箇所でclos->esc = func->esc;という行が追加されています。

  1. OCOMPLITノードが作成された直後。
  2. OCONVNOPノード(型変換を表すノード)が作成され、そのleftフィールドにPTRLITノード(ポインタリテラル)が挿入された後。

これにより、クロージャが生成される段階で、そのクロージャが属する関数のエスケープ解析の結果がクロージャノードに引き継がれます。コンパイラの後のフェーズで、このエスケープ情報が利用され、クロージャがヒープに割り当てる必要がないと判断された場合、スタックに割り当てられるようになります。

例えば、クロージャが関数内で定義され、その関数内でのみ呼び出され、かつキャプチャする変数がすべてスタックに割り当て可能である場合、この変更によってクロージャ自体もスタックに割り当てられる可能性が高まります。これにより、不要なヒープ割り当てが削減され、ガベージコレクションの頻度と実行時間が減少するため、全体的なパフォーマンスが向上します。

src/pkg/sort/search_test.goの変更は、この最適化の効果を測定するためのベンチマークとテストの追加です。runSearchWrappers関数は、sortパッケージの様々なSearch関数を呼び出しており、これらの関数は内部でクロージャを使用しています。TestSearchWrappersDontAllocは、testing.AllocsPerRunを使用して、runSearchWrappersがメモリ割り当てを行わないことを検証しています。これは、エスケープ解析が正しく機能し、クロージャがヒープにエスケープしないことを確認するための重要なテストです。

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

src/cmd/gc/closure.c

--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -238,14 +238,18 @@ walkclosure(Node *func, NodeList **init)
 	}
 
 	clos = nod(OCOMPLIT, N, nod(OIND, typ, N));
+	clos->esc = func->esc; // ★追加箇所1: OCOMPLITノードにエスケープ情報を伝播
 	clos->right->implicit = 1;
 	clos->list = concat(list1(nod(OCFUNC, func->closure->nname, N)), func->enter);
 
 	// Force type conversion from *struct to the func type.
 	clos = nod(OCONVNOP, clos, N);
 	clos->type = func->type;
-	
+
 	typecheck(&clos, Erv);
+	// typecheck will insert a PTRLIT node under CONVNOP,
+	// tag it with escape analysis result.
+	clos->left->esc = func->esc; // ★追加箇所2: PTRLITノードにエスケープ情報を伝播
 	walkexpr(&clos, init);
 
 	return clos;

src/pkg/sort/search_test.go

--- a/src/pkg/sort/search_test.go
+++ b/src/pkg/sort/search_test.go
@@ -117,6 +117,28 @@ func TestSearchWrappers(t *testing.T) {
 	}
 }
 
+func runSearchWrappers() {
+	SearchInts(data, 11)
+	SearchFloat64s(fdata, 2.1)
+	SearchStrings(sdata, "")
+	IntSlice(data).Search(0)
+	Float64Slice(fdata).Search(2.0)
+	StringSlice(sdata).Search("x")
+}
+
+func TestSearchWrappersDontAlloc(t *testing.T) {
+	allocs := testing.AllocsPerRun(100, runSearchWrappers)
+	if allocs != 0 {
+		t.Errorf("expected no allocs for runSearchWrappers, got %v", allocs)
+	}
+}
+
+func BenchmarkSearchWrappers(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		runSearchWrappers()
+	}
+}
+
 // Abstract exhaustive test: all sizes up to 100,
 // all possible return values.  If there are any small
 // corner cases, this test exercises them.

コアとなるコードの解説

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

walkclosure関数は、Goコンパイラがクロージャリテラルを処理する際に呼び出される重要な関数です。この関数は、クロージャを表すASTノードを構築します。

  1. clos = nod(OCOMPLIT, N, nod(OIND, typ, N)); の直後: OCOMPLITは、複合リテラル(ここではクロージャ)を表すノードです。このノードが作成された直後に clos->esc = func->esc; が追加されました。 func->escは、このクロージャが定義されている親関数(func)のエスケープ解析の結果(エスケープするかしないか)を保持しています。この行により、親関数のエスケープ情報が、新しく作成されるクロージャノードに引き継がれます。これは、クロージャが親関数と同じエスケープ特性を持つべきであるというコンパイラのヒューリスティックを反映しています。例えば、親関数がスタックに割り当てられるべきと判断された場合、その内部で一時的に使用されるクロージャもスタックに割り当てられる可能性が高まります。

  2. typecheck(&clos, Erv); の直後: typecheckは、ノードの型を決定し、必要に応じてASTを変換するコンパイラのフェーズです。クロージャの場合、typecheckは内部的にOCONVNOP(型変換)ノードの下にPTRLIT(ポインタリテラル)ノードを挿入することがあります。このPTRLITノードは、クロージャの実体へのポインタを表します。 clos->left->esc = func->esc; は、このPTRLITノードに対しても、親関数のエスケープ情報を伝播させています。これにより、クロージャの実体へのポインタ自体がエスケープするかどうかの判断にも、親関数のエスケープ解析の結果が考慮されるようになります。

これらの変更により、コンパイラはクロージャの生存期間をより正確に推論できるようになり、クロージャがヒープに割り当てられる必要がない場合に、スタックに割り当てることが可能になります。結果として、不要なメモリ割り当てが削減され、ガベージコレクションの負荷が軽減されます。

src/pkg/sort/search_test.go の変更

このファイルへの変更は、主に新しいベンチマークとテストの追加です。

  1. runSearchWrappers() 関数: sortパッケージの様々なSearch関数(SearchInts, SearchFloat64s, SearchStringsなど)を呼び出すヘルパー関数です。これらのSearch関数は、内部でクロージャを使用して探索ロジックを実装しています。

  2. TestSearchWrappersDontAlloc(t *testing.T) 関数: このテストは、testing.AllocsPerRunユーティリティ関数を使用しています。AllocsPerRun(N, f)は、関数fN回実行した際の平均メモリ割り当て回数を返します。 このテストでは、runSearchWrappers関数が100回実行されたときに、メモリ割り当てが0であることを期待しています。もし割り当てが発生した場合、テストは失敗します。これは、このコミットによるエスケープ解析の改善が正しく機能し、Search関数内で使用されるクロージャがヒープにエスケープせず、スタックに割り当てられていることを検証するためのものです。

  3. BenchmarkSearchWrappers(b *testing.B) 関数: runSearchWrappers関数をベンチマークとして実行します。このベンチマークの結果は、コミットメッセージに記載されているパフォーマンス改善の数値(BenchmarkSearchWrappersdelta)の根拠となります。エスケープ解析の改善により、このベンチマークの実行時間が大幅に短縮されることが期待されます。

これらのテストとベンチマークの追加は、コンパイラの最適化が実際にパフォーマンスに貢献していることを数値的に証明し、回帰テストとしても機能します。

関連リンク

  • Go Issue #3520: https://code.google.com/p/go/issues/detail?id=3520 (古いGoプロジェクトのIssueトラッカーのリンクですが、現在はGitHubに移行しています。GitHub上での対応するIssueは通常、元のIssue番号で検索可能です。)
  • Gerrit Change-ID 7397056: https://golang.org/cl/7397056 (GoプロジェクトのコードレビューシステムであるGerritのリンク。コミットの詳細な変更履歴やレビューコメントを確認できます。)

参考にした情報源リンク

  • Go言語のエスケープ解析に関する一般的な情報源(例: Go公式ブログ、Go言語の書籍、技術記事など)
  • Go言語のクロージャに関する一般的な情報源
  • Goコンパイラの内部構造に関するドキュメントや解説
  • testingパッケージのAllocsPerRun関数のドキュメント

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

このコミットは、Goコンパイラのガベージコレクション(gc)ツールにおけるエスケープ解析の改善に関するものです。特に、クロージャに対するエスケープ解析の結果を適用することで、特定のシナリオでのメモリ割り当てを削減し、パフォーマンスを向上させています。

コミット

commit 9fe60801aeca801010b42e1dd7ad57a173dc9740
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Feb 26 00:40:28 2013 +0100

    cmd/gc: apply escape analysis results to closures.
    
    This avoids an allocation when closures are used
    as "macros", in Walk idioms, or as argument to defer.
    
    benchmark                old ns/op    new ns/op    delta
    BenchmarkSearchWrappers       1171          354  -69.77%
    BenchmarkCallClosure             3            3  -12.54%
    BenchmarkCallClosure1          119            7  -93.95%
    BenchmarkCallClosure2          183           74  -59.18%
    BenchmarkCallClosure3          187           75  -59.57%
    BenchmarkCallClosure4          187           76  -58.98%
    
    Compared to Go 1:
    benchmark                  old ns/op    new ns/op    delta
    BenchmarkSearchWrappers         3208          354  -88.97%
    
    Fixes #3520.
    
    R=daniel.morsing, bradfitz, minux.ma, dave, rsc
    CC=golang-dev
    https://golang.org/cl/7397056

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

https://github.com/golang/go/commit/9fe60801aeca801010b42e1dd7ad57a173dc9740

元コミット内容

Goコンパイラ(cmd/gc)において、クロージャに対するエスケープ解析の結果を適用するように変更しました。これにより、クロージャが「マクロ」として、Walkイディオムで、またはdeferの引数として使用される場合に、不要なメモリ割り当てを回避します。

この変更により、ベンチマークで顕著なパフォーマンス改善が見られました。特にBenchmarkSearchWrappersでは約70%の高速化、BenchmarkCallClosure1では90%以上の高速化を達成しています。Go 1と比較しても、BenchmarkSearchWrappersで約89%の高速化が確認されています。

このコミットは、Issue #3520を修正するものです。

変更の背景

Go言語では、クロージャは非常に強力な機能であり、コードの簡潔性や表現力を高めるために広く利用されます。しかし、クロージャがキャプチャする変数は、そのクロージャがスコープ外で実行される可能性があるため、ヒープに割り当てられる(エスケープする)必要があります。これは、ガベージコレクションのオーバーヘッドを増加させ、パフォーマンスに影響を与える可能性があります。

このコミットの背景には、Goコンパイラのエスケープ解析がクロージャに対して十分に最適化されていなかったという問題がありました。特に、クロージャが一時的な目的で、例えばループ内で「マクロ」のように使われたり、defer文の引数として使われたりする場合でも、不必要にヒープ割り当てが発生していました。これは、コンパイラがクロージャの生存期間を正確に判断できず、安全側に倒してヒープに割り当てていたためです。

Issue #3520は、まさにこの問題、すなわちクロージャが不必要にヒープに割り当てられることによるパフォーマンスの低下を指摘していました。このコミットは、その問題を解決し、Goプログラムの実行効率を向上させることを目的としています。

前提知識の解説

エスケープ解析 (Escape Analysis)

エスケープ解析は、コンパイラ最適化の一種で、プログラム内の変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定するプロセスです。

  • スタック割り当て: 関数内で宣言されたローカル変数は、通常、その関数が実行されている間だけ有効なメモリ領域であるスタックに割り当てられます。関数が終了すると、スタックフレームが破棄され、その変数が占めていたメモリは自動的に解放されます。これは非常に高速で、ガベージコレクションのオーバーヘッドがありません。
  • ヒープ割り当て: 変数がその宣言されたスコープ(関数)の外でも参照され続ける可能性がある場合(例えば、ポインタが関数から返される場合や、グローバル変数に代入される場合など)、その変数はヒープに割り当てられます。ヒープはプログラム全体で共有されるメモリ領域であり、ガベージコレクタによって管理されます。ヒープ割り当てはスタック割り当てよりもコストが高く、ガベージコレクションの対象となるため、パフォーマンスに影響を与える可能性があります。

エスケープ解析の目的は、可能な限り多くの変数をスタックに割り当てることで、ヒープ割り当ての回数を減らし、ガベージコレクションの負荷を軽減し、プログラムの実行速度を向上させることです。

クロージャ (Closures)

クロージャは、関数と、その関数が宣言された環境(レキシカルスコープ)を組み合わせたものです。クロージャは、その定義時に存在していた非ローカル変数(自由変数)を「キャプチャ」し、その変数をクロージャが実行される際にも利用できるようにします。

Go言語において、クロージャがキャプチャする変数は、そのクロージャが宣言された関数が終了した後も生存し続ける必要があるため、通常はヒープに割り当てられます。しかし、クロージャがその宣言された関数内でのみ使用され、その関数の終了とともに寿命が尽きるような場合、ヒープ割り当ては不要であり、スタックに割り当てることが可能です。

Walk イディオム

Goコンパイラ(cmd/gc)の内部では、AST(抽象構文木)を走査(Walk)する際に、特定の処理を適用するためにクロージャが頻繁に利用されます。これは、ツリー構造を再帰的に探索し、各ノードでカスタムのロジックを実行する一般的なパターンです。このような内部的な処理で生成されるクロージャが、不必要にヒープに割り当てられると、コンパイラ自身のパフォーマンスにも影響を与えます。

defer

defer文は、その関数がリターンする直前、またはパニックが発生した場合に実行される関数呼び出しをスケジュールします。deferに渡される関数は、多くの場合、クロージャです。例えば、リソースの解放(ファイルのクローズ、ロックの解除など)によく使われます。deferに渡されるクロージャが不必要にヒープに割り当てられると、関数の終了時に余分なガベージコレクションのオーバーヘッドが発生する可能性があります。

技術的詳細

このコミットの技術的詳細の中心は、Goコンパイラのcmd/gcにおけるクロージャの生成とエスケープ解析の連携を改善することです。

Goコンパイラは、ソースコードをASTに変換し、そのASTに対して様々な最適化パスを実行します。エスケープ解析もその一つです。クロージャがコード内で定義されると、コンパイラはそれを内部的に表現する構造を生成します。この構造は、クロージャのコード自体と、キャプチャされた変数への参照を含みます。

変更前は、クロージャが生成される際に、そのクロージャがヒープにエスケープするかどうかの判断が、クロージャの生成ロジックとエスケープ解析の結果との間で十分に連携していませんでした。特に、walkclosure関数(src/cmd/gc/closure.c内)は、クロージャを表すノード(OCOMPLIT)を生成し、そのノードを型チェック(typecheck)に渡します。型チェックの後、さらにwalkexprが呼び出されます。

このコミットの変更は、walkclosure関数内で、クロージャノード(clos)に、そのクロージャが属する関数(func)のエスケープ解析結果(func->esc)を伝播させるようにしています。具体的には、以下の2箇所でclos->esc = func->esc;という行が追加されています。

  1. OCOMPLITノードが作成された直後。
  2. OCONVNOPノード(型変換を表すノード)が作成され、そのleftフィールドにPTRLITノード(ポインタリテラル)が挿入された後。

これにより、クロージャが生成される段階で、そのクロージャが属する関数のエスケープ解析の結果がクロージャノードに引き継がれます。コンパイラの後のフェーズで、このエスケープ情報が利用され、クロージャがヒープに割り当てる必要がないと判断された場合、スタックに割り当てられるようになります。

例えば、クロージャが関数内で定義され、その関数内でのみ呼び出され、かつキャプチャする変数がすべてスタックに割り当て可能である場合、この変更によってクロージャ自体もスタックに割り当てられる可能性が高まります。これにより、不要なヒープ割り当てが削減され、ガベージコレクションの頻度と実行時間が減少するため、全体的なパフォーマンスが向上します。

src/pkg/sort/search_test.goの変更は、この最適化の効果を測定するためのベンチマークとテストの追加です。runSearchWrappers関数は、sortパッケージの様々なSearch関数を呼び出しており、これらの関数は内部でクロージャを使用しています。TestSearchWrappersDontAllocは、testing.AllocsPerRunを使用して、runSearchWrappersがメモリ割り当てを行わないことを検証しています。これは、エスケープ解析が正しく機能し、クロージャがヒープにエスケープしないことを確認するための重要なテストです。

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

src/cmd/gc/closure.c

--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -238,14 +238,18 @@ walkclosure(Node *func, NodeList **init)
 	}
 
 	clos = nod(OCOMPLIT, N, nod(OIND, typ, N));
+	clos->esc = func->esc; // ★追加箇所1: OCOMPLITノードにエスケープ情報を伝播
 	clos->right->implicit = 1;
 	clos->list = concat(list1(nod(OCFUNC, func->closure->nname, N)), func->enter);
 
 	// Force type conversion from *struct to the func type.
 	clos = nod(OCONVNOP, clos, N);
 	clos->type = func->type;
-	
+
 	typecheck(&clos, Erv);
+	// typecheck will insert a PTRLIT node under CONVNOP,
+	// tag it with escape analysis result.
+	clos->left->esc = func->esc; // ★追加箇所2: PTRLITノードにエスケープ情報を伝播
 	walkexpr(&clos, init);
 
 	return clos;

src/pkg/sort/search_test.go

--- a/src/pkg/sort/search_test.go
+++ b/src/pkg/sort/search_test.go
@@ -117,6 +117,28 @@ func TestSearchWrappers(t *testing.T) {
 	}
 }
 
+func runSearchWrappers() {
+	SearchInts(data, 11)
+	SearchFloat64s(fdata, 2.1)
+	SearchStrings(sdata, "")
+	IntSlice(data).Search(0)
+	Float64Slice(fdata).Search(2.0)
+	StringSlice(sdata).Search("x")
+}
+
+func TestSearchWrappersDontAlloc(t *testing.T) {
+	allocs := testing.AllocsPerRun(100, runSearchWrappers)
+	if allocs != 0 {
+		t.Errorf("expected no allocs for runSearchWrappers, got %v", allocs)
+	}
+}
+
+func BenchmarkSearchWrappers(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		runSearchWrappers()
+	}
+}
+
 // Abstract exhaustive test: all sizes up to 100,
 // all possible return values.  If there are any small
 // corner cases, this test exercises them.

コアとなるコードの解説

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

walkclosure関数は、Goコンパイラがクロージャリテラルを処理する際に呼び出される重要な関数です。この関数は、クロージャを表すASTノードを構築します。

  1. clos = nod(OCOMPLIT, N, nod(OIND, typ, N)); の直後: OCOMPLITは、複合リテラル(ここではクロージャ)を表すノードです。このノードが作成された直後に clos->esc = func->esc; が追加されました。 func->escは、このクロージャが定義されている親関数(func)のエスケープ解析の結果(エスケープするかしないか)を保持しています。この行により、親関数のエスケープ情報が、新しく作成されるクロージャノードに引き継がれます。これは、クロージャが親関数と同じエスケープ特性を持つべきであるというコンパイラのヒューリスティックを反映しています。例えば、親関数がスタックに割り当てられるべきと判断された場合、その内部で一時的に使用されるクロージャもスタックに割り当てられる可能性が高まります。

  2. typecheck(&clos, Erv); の直後: typecheckは、ノードの型を決定し、必要に応じてASTを変換するコンパイラのフェーズです。クロージャの場合、typecheckは内部的にOCONVNOP(型変換)ノードの下にPTRLIT(ポインタリテラル)ノードを挿入することがあります。このPTRLITノードは、クロージャの実体へのポインタを表します。 clos->left->esc = func->esc; は、このPTRLITノードに対しても、親関数のエスケープ情報を伝播させています。これにより、クロージャの実体へのポインタ自体がエスケープするかどうかの判断にも、親関数のエスケープ解析の結果が考慮されるようになります。

これらの変更により、コンパイラはクロージャの生存期間をより正確に推論できるようになり、クロージャがヒープに割り当てられる必要がない場合に、スタックに割り当てることが可能になります。結果として、不要なメモリ割り当てが削減され、ガベージコレクションの負荷が軽減されます。

src/pkg/sort/search_test.go の変更

このファイルへの変更は、主に新しいベンチマークとテストの追加です。

  1. runSearchWrappers() 関数: sortパッケージの様々なSearch関数(SearchInts, SearchFloat64s, SearchStringsなど)を呼び出すヘルパー関数です。これらのSearch関数は、内部でクロージャを使用して探索ロジックを実装しています。

  2. TestSearchWrappersDontAlloc(t *testing.T) 関数: このテストは、testing.AllocsPerRunユーティリティ関数を使用しています。AllocsPerRun(N, f)は、関数fN回実行した際の平均メモリ割り当て回数を返します。 このテストでは、runSearchWrappers関数が100回実行されたときに、メモリ割り当てが0であることを期待しています。もし割り当てが発生した場合、テストは失敗します。これは、このコミットによるエスケープ解析の改善が正しく機能し、Search関数内で使用されるクロージャがヒープにエスケープせず、スタックに割り当てられていることを検証するためのものです。

  3. BenchmarkSearchWrappers(b *testing.B) 関数: runSearchWrappers関数をベンチマークとして実行します。このベンチマークの結果は、コミットメッセージに記載されているパフォーマンス改善の数値(BenchmarkSearchWrappersdelta)の根拠となります。エスケープ解析の改善により、このベンチマークの実行時間が大幅に短縮されることが期待されます。

これらのテストとベンチマークの追加は、コンパイラの最適化が実際にパフォーマンスに貢献していることを数値的に証明し、回帰テストとしても機能します。

関連リンク

  • Go Issue #3520: https://code.google.com/p/go/issues/detail?id=3520 (このIssueは古いGoプロジェクトのIssueトラッカーに存在していたもので、現在のGitHubリポジトリには直接同じ番号で移行されていません。そのため、GitHubで検索しても関連性の低い結果が表示される可能性があります。)
  • Gerrit Change-ID 7397056: https://golang.org/cl/7397056 (GoプロジェクトのコードレビューシステムであるGerritのリンク。コミットの詳細な変更履歴やレビューコメントを確認できます。)

参考にした情報源リンク

  • Go言語のエスケープ解析に関する一般的な情報源(例: Go公式ブログ、Go言語の書籍、技術記事など)
  • Go言語のクロージャに関する一般的な情報源
  • Goコンパイラの内部構造に関するドキュメントや解説
  • testingパッケージのAllocsPerRun関数のドキュメント