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

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

このコミットは、Goコンパイラのcmd/gcにおけるエスケープ解析の順序付けに関するバグ修正です。具体的には、//go:noescapeディレクティブが適用された関数が、ソースコード内での宣言順序に依存せずに正しくエスケープ解析されるように改善されました。

コミット

commit 148fac79a33bf7e9be279002aa289eacad41cb8f
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jun 25 17:28:49 2013 -0400

    cmd/gc: fix escape analysis ordering
    
    Functions without bodies were excluded from the ordering logic,
    because when I wrote the ordering logic there was no reason to
    analyze them.
    
    But then we added //go:noescape tags that need analysis, and we
    didn't update the ordering logic.
    
    So in the absence of good ordering, //go:noescape only worked
    if it appeared before the use in the source code.
    
    Fixes #5773.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/10570043

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

https://github.com/golang/go/commit/148fac79a33bf7e9be279002aa289eacad41cb8f

元コミット内容

cmd/gc: エスケープ解析の順序付けを修正

関数本体を持たない関数は、順序付けロジックから除外されていました。なぜなら、私が順序付けロジックを書いたとき、それらを解析する理由はなかったからです。

しかし、その後、解析が必要な//go:noescapeタグを追加しましたが、順序付けロジックを更新しませんでした。

そのため、適切な順序付けがない場合、//go:noescapeはソースコード内で使用される前に現れた場合にのみ機能していました。

Issue #5773 を修正します。

変更の背景

Goコンパイラには、プログラムのパフォーマンスを最適化するための「エスケープ解析 (Escape Analysis)」という重要なプロセスがあります。エスケープ解析は、変数がヒープに割り当てられるべきか、それともスタックに割り当てられるべきかを決定します。スタック割り当てはヒープ割り当てよりも高速であり、ガベージコレクションの負荷を軽減するため、可能な限りスタック割り当てが望ましいとされます。

//go:noescapeというコンパイラディレクティブは、特定の関数が引数として受け取ったポインタが、その関数の呼び出し元スコープの外に「エスケープ」しないことをコンパイラに明示的に伝えるために使用されます。これにより、コンパイラはこれらの引数をスタックに割り当てることができ、パフォーマンスが向上する可能性があります。このディレクティブは、特にアセンブリで書かれた関数や、Goのランタイム内部で非常にパフォーマンスが要求されるコードで使用されます。

このコミット以前は、Goコンパイラのエスケープ解析の順序付けロジックに問題がありました。具体的には、関数本体を持たない(例えば、アセンブリで実装された)関数は、エスケープ解析の順序付けの対象から除外されていました。これは、元々そのような関数を解析する必要がなかったためです。しかし、後に//go:noescapeディレクティブが導入され、これらの「本体を持たない関数」に対してもエスケープ解析の考慮が必要になりました。

この不整合により、//go:noescapeが付けられた関数が、その関数が呼び出される前にソースコード上で宣言されていない場合、コンパイラが正しくエスケープ解析を行えないというバグが発生していました。結果として、//go:noescapeの意図した最適化効果が得られない、または誤ったエスケープ解析が行われる可能性がありました。この問題は、GoのIssue #5773として報告されていました。

このコミットは、この順序付けの問題を解決し、//go:noescapeディレクティブがソースコード内の宣言順序に依存せず、常に正しく機能するようにすることを目的としています。

前提知識の解説

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

Goコンパイラの重要な最適化の一つで、変数がメモリのどこに割り当てられるべきかを決定します。

  • スタック割り当て: 関数呼び出し時に確保され、関数が終了すると自動的に解放されるメモリ領域。高速で、ガベージコレクションの対象外。
  • ヒープ割り当て: プログラム実行中に動的に確保されるメモリ領域。ガベージコレクタによって管理され、解放される。スタックに比べてアクセスが遅く、GCのオーバーヘッドが発生する。

エスケープ解析は、変数のライフタイムを分析し、その変数が関数のスコープ外に「エスケープ」するかどうかを判断します。

  • エスケープしない場合: スタックに割り当てられる。
  • エスケープする場合: ヒープに割り当てられる。

例えば、関数のローカル変数のアドレスが関数の戻り値として返される場合、その変数は関数終了後も参照され続ける可能性があるため、ヒープにエスケープすると判断されます。

2. //go:noescape ディレクティブ

Goコンパイラに対する特別な指示(プラグマ)の一つです。関数宣言の直前に記述することで、その関数が引数として受け取ったポインタが、関数の呼び出し元スコープの外にエスケープしないことをコンパイラに保証します。

例:

//go:noescape
func MyFunction(ptr *int)

このディレクティブは、主に以下のような場合に利用されます。

  • アセンブリ関数: Goで実装されていないが、Goのコードから呼び出されるアセンブリ言語で書かれた関数。コンパイラはこれらの関数の内部動作を解析できないため、開発者が明示的にエスケープ挙動を保証する必要があります。
  • パフォーマンスクリティカルなコード: 非常に高速な実行が求められるコードで、ヒープ割り当てを避けたい場合。

//go:noescapeが正しく機能すると、コンパイラは引数として渡されたポインタが指すデータをスタックに割り当てることができ、ガベージコレクションの負荷を軽減し、実行速度を向上させることができます。しかし、この保証が誤っている場合(実際にはエスケープするのにnoescapeと宣言した場合)、プログラムの動作が不正になったり、メモリリークが発生したりする可能性があります。

3. Goコンパイラの構造 (cmd/gc)

cmd/gcは、Go言語の公式コンパイラです。コンパイルプロセスは複数のフェーズに分かれており、エスケープ解析はその最適化フェーズの一部として実行されます。コンパイラは、ソースコードを抽象構文木(AST)に変換し、型チェック、エスケープ解析、最適化、コード生成などのステップを経て、最終的に実行可能なバイナリを生成します。

4. NodePFUNCONAMEdefnnbody

Goコンパイラの内部では、プログラムの構造はNodeというデータ構造で表現されます。NodeはASTの各要素(関数、変数、式など)を表します。

  • fn->op == ONAME: fnが名前(変数名、関数名など)を表すノードであることを示します。
  • fn->class == PFUNC: fnが関数であることを示します。
  • fn->defn: 関数の定義(本体)へのポインタです。関数本体がない場合(例: 外部リンケージのアセンブリ関数)はnilになることがあります。
  • fn->defn->nbody: 関数の本体(ASTのノードリスト)が存在するかどうかを示すフラグまたはポインタです。関数本体がない場合はnilまたは空になります。

このコミットの変更は、エスケープ解析を行うesc.cファイル内のロジックに影響を与えます。

技術的詳細

このコミットの核心は、src/cmd/gc/esc.cファイル内のvisitcode関数における条件式の変更です。visitcode関数は、エスケープ解析の過程でコードを走査し、関数呼び出しなどを処理します。

変更前のコードでは、関数呼び出しの対象となる関数fnがエスケープ解析の対象となるかどうかを判断する際に、以下の条件が含まれていました。

if(fn && fn->op == ONAME && fn->class == PFUNC && fn->defn && fn->defn->nbody)

この条件式は、関数fnが名前を持ち、関数であり、かつ関数定義(fn->defn)が存在し、さらにその関数定義が本体(fn->defn->nbody)を持っている場合にのみ、その関数に対してエスケープ解析のvisit処理(visit(fn->defn))を実行していました。

問題は、//go:noescapeディレクティブが適用される関数は、Goのソースコード上では関数本体を持たない(例えば、アセンブリで実装されているため)場合がある点です。このような関数はfn->defn->nbodynilまたは空となり、上記の条件式によってエスケープ解析の対象から除外されていました。

コミットの変更は、この条件式から&& fn->defn->nbodyの部分を削除することです。

if(fn && fn->op == ONAME && fn->class == PFUNC && fn->defn)

この変更により、関数fnが名前を持ち、関数であり、かつ関数定義(fn->defn)が存在するだけで、エスケープ解析のvisit処理が実行されるようになります。つまり、関数本体の有無にかかわらず、関数定義が存在すればエスケープ解析の順序付けロジックに含まれるようになります。

これにより、//go:noescapeディレクティブが適用された関数(本体を持たない可能性がある)も、エスケープ解析の順序付けの対象となり、コンパイラがそのディレクティブを正しく解釈し、適切な最適化を適用できるようになります。結果として、//go:noescapeがソースコード内の宣言順序に依存せず、常に意図した通りに機能するようになります。

テストファイルtest/escape2.goの追加は、この修正が正しく機能することを確認するためのものです。このテストでは、//go:noescapeが付けられた関数foo144afoo144bが、それぞれ呼び出し元よりも前に宣言されている場合と後に宣言されている場合の両方で、ポインタがエスケープしないことをコンパイラが正しく検出できることを検証しています。

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

変更はsrc/cmd/gc/esc.cファイル内のvisitcode関数にあります。

--- a/src/cmd/gc/esc.c
+++ b/src/cmd/gc/esc.c
@@ -144,7 +144,7 @@ visitcode(Node *n, uint32 min)
 		fn = n->left;
 		if(n->op == OCALLMETH)
 			fn = n->left->right->sym->def;
-		if(fn && fn->op == ONAME && fn->class == PFUNC && fn->defn && fn->defn->nbody)
+		if(fn && fn->op == ONAME && fn->class == PFUNC && fn->defn)
 			if((m = visit(fn->defn)) < min)
 				min = m;
 	}

コアとなるコードの解説

上記の差分は、visitcode関数内のif文の条件式を変更しています。

  • 変更前: if(fn && fn->op == ONAME && fn->class == PFUNC && fn->defn && fn->defn->nbody) この条件は、fnが有効な関数名ノードであり、関数クラスを持ち、かつその関数が定義(defn)を持ち、さらにその定義が本体(nbody)を持っている場合に真となります。

  • 変更後: if(fn && fn->op == ONAME && fn->class == PFUNC && fn->defn) この条件は、fnが有効な関数名ノードであり、関数クラスを持ち、かつその関数が定義(defn)を持っている場合に真となります。fn->defn->nbodyのチェックが削除されています。

この変更の意図は、関数本体を持たない関数(例えば、//go:noescapeが適用されるアセンブリ関数など)も、エスケープ解析の順序付けロジックに含めることです。以前のロジックでは、関数本体がないためにこれらの関数がエスケープ解析の対象から外れてしまい、//go:noescapeディレクティブが正しく機能しない原因となっていました。

fn->defnが存在するということは、その関数がコンパイラによって認識され、その定義情報(シグネチャなど)が利用可能であることを意味します。nbodyのチェックを削除することで、コンパイラは関数本体の有無にかかわらず、//go:noescapeのようなディレクティブを考慮したエスケープ解析を適用できるようになります。これにより、//go:noescapeがソースコード内の宣言順序に依存せず、常に期待通りに動作するようになります。

関連リンク

  • Go Issue #5773: https://code.google.com/p/go/issues/detail?id=5773 (Google Code Archive)
    • このコミットが修正した具体的なバグ報告です。詳細な議論や再現手順が記載されている可能性があります。
  • Go CL 10570043: https://golang.org/cl/10570043 (Go Code Review)
    • このコミットに対応するGoのコードレビューページです。レビューコメントや変更の経緯に関する追加情報が含まれている場合があります。

参考にした情報源リンク

  • 上記のGitHubコミットページ: https://github.com/golang/go/commit/148fac79a33bf7e9be279002aa289eacad41cb8f
  • Go言語のエスケープ解析に関する一般的な情報源 (例: Go公式ドキュメント、ブログ記事など)
  • //go:noescapeディレクティブに関する情報源 (例: Go公式ドキュメント、Goのソースコードコメントなど)
  • Goコンパイラの内部構造に関する情報源 (例: Goのソースコード、コンパイラ設計に関する論文や記事など)