プログラミング
Goにおけるエスケープ解析:スタック対ヒープ割り当ての説明
Escape Analysis in Go: Stack vs. Heap Allocations Explained (blog.jetbrains.com)
要約
この記事では、Goのコンパイラ最適化であるエスケープ解析について解説します。これは、値がスタックに割り当てられるか、ヒープに移動する必要があるかを決定するプロセスです。スタックは高速で一時的、ヒープは共有されGCの対象となるためリソースを消費します。関数が終了しても参照が残る可能性がある値はヒープに移動します。一般的なエスケープのケース(ポインタの返却、クロージャ、インターフェース、スライス・マップ・構造体など)と、コンパイラフラグを使った確認方法を説明します。
全文翻訳
GoLand プロフェッショナルなGo開発のためのIDE
フォロー フォロー: X X Youtube Youtube RSS RSS slack slack GoLandをダウンロード
Goにおけるエスケープ解析 – スタック対ヒープ割り当ての説明
Dominika Stankiewicz
GoogleがGoを開発する際に採用した設計上の選択の1つは、開発者からメモリ管理を抽象化し、彼らが本当に重要なこと、つまりコードを書くことに集中できるようにすることでした。エスケープ解析やガベージコレクションのようなものは自動的に行われ、Goコンパイラはほとんど神秘的な方法で動作します。これはGoの最も優れた機能の1つですが、プログラムがうまく機能している限りです。しかし、メモリの問題が発生し、プロセスを解明して最適化する必要がある場合、視点は変わり、その神秘はもはやそれほど魅力的ではなくなります。
この記事では、最も混乱しやすいパフォーマンス最適化の問題の1つであるエスケープ解析、つまりコンパイラが何がスタックに残り、何がヒープに移動するかを決定する方法を説明します。エスケープ解析とは何か、どのように機能するか、最も一般的なエスケープケースとは何か、それらをどのように検査するか、なぜ検査が使いにくい場合があるか、そしてGoLandがどのように役立つかについても説明します。
Goにおけるエスケープ解析とは?
エスケープ解析とは、値がスタックに割り当てられるか、ヒープに移動する必要があるかを決定するコンパイラ最適化です。言い換えれば、Golangでは、エスケープ解析プロセスは、プログラムが作成するすべての値を検査して、「この値は安全にスタック上に存在できるか、それとも関数が返された後も現在の関数外の何かがそれを必要とするか(したがってヒープ上に存在する必要があるか)」という質問に答えます。
スタックは、割り当てが大幅に安価で、関数が返されると自動的に解放される、ゴルーチンごとの領域です。そのため、ストレージは高速ですが、短命です。ヒープは共有され、長命なメモリ空間であり、ガベージコレクタが追跡およびクリーンアップする必要があるため、より多くのリソースを消費します。エスケープ解析は、この2つの間の橋渡しです。
Goのドキュメントで説明されているように、値が関数を終了するまでに使用されなくなったとコンパイラが証明できない場合、その値は「エスケープ」したと言われます。各値について、その値への参照が作成した関数よりも長く存続できるかどうかを質問します。答えが「いいえ」の場合、値はスタック上に残ります。答えが「はい」の場合、またはコンパイラが「いいえ」と証明できない場合、値は安全のためにヒープに割り当てられます。
典型的な例は、ローカル変数のポインタを返すことです。関数は終了しますが、その変数への参照が残るため、値は破棄されようとしているスタックフレーム上に存在できません。代わりにヒープにエスケープします。
エスケープの決定は固定されたものではないことをここで指摘しておく価値があります。それらは、コードの構造、コンパイルに使用するGoのバージョン、環境(OS/アーキテクチャ)、コンパイラ設定、およびインライニングなどの他の最適化決定によって変化する可能性があります。そのため、特定のコンテキストで値が常にエスケープするかしないかを想定することはできません。毎回確認する必要があります。
なぜ気にする必要があるのか?
他のいくつかの言語とは異なり、Goでは、Cのmallocとfreeのようにスタックとヒープの割り当てを明示的に選択することはありません。代わりに、コンパイラが決定を下します。Goはメモリの安全性も自動的に管理するため、エスケープした値が安全でなくなることを防ぎます。多くの開発者はそこで止まり、エスケープ解析を気にかけることはありません。結局のところ、ドキュメントには「知る必要はない」と書かれており、動作するなら動作する、ですよね?
とはいえ、あなたはまだ主体性を持っており、これらのコンパイラ決定に影響を与える方法でコードを書くことができます。良い方法でも、悪い方法でも。そのため、エスケープ解析の理解は、実際にはすべてのGo開発者にとって必須のスキルです。
値がヒープにエスケープする一般的な理由
ほとんどの場合、値がヒープにエスケープするのは、いくつかの繰り返し発生する理由のいずれかです。これらのパターンを認識すると、コンパイラ出力をより速く読み取ることができ、特定の割り当てが調査を必要とするか、単にコードを実行するための最良の方法であるかがわかります。すべてのエスケープが問題であるわけではないことに注意することが重要です。ある程度の複雑さを持つプログラムには、必然的にヒープ上に存在するものがいくつかあります。ここでの目標は、パターンを認識することであり、それらをすべて排除することではありません。
ポインタの返却
ローカル値へのポインタを返すことは、おそらくエスケープの最も一般的な原因です。値は関数内で作成されますが、呼び出し元は関数が返された後も参照を保持するため、破棄されるスタックフレーム上に存在できません。
func NewUser(name string) *User {
u := User{Name: name} // uはヒープにエスケープします
return &u
}
これはGoでは安全です。コンパイラは、&uがNewUserよりも長く存続することに気づき、値を自動的にヒープに移動します。気にするかどうかは文脈によります。ポインタを返すことは慣用的であり、APIの明確さと可読性にとってしばしば正しい選択です。正しい選択は、ポインタを避けることに関する包括的なルールではなく、API設計と測定されたパフォーマンスへの影響に依存します。
クロージャとゴルーチン
クロージャまたはゴルーチンが作成した関数よりも長く存続する可能性がある場合、キャプチャされた変数はエスケープします。コンパイラは、キャプチャされた値がまだ到達可能であると仮定する必要があるため、それをヒープに割り当てます。
func process(data []byte) {
go func() {
handle(data) // dataはエスケープする可能性があります: ゴルーチンはプロセスよりも長く存続する可能性があります
}()
}
ゴルーチンは、親関数が返された後も実行を続けることができるため、ここで混乱の頻繁な原因となります。コンパイラの観点からは、ゴルーチンが触れるものはすべて無期限に必要になる可能性があるため、安全策を講じます。
インターフェースと動的な値
インターフェースを介して具体的な値を渡すと、ヒープ割り当てにつながることがあります。これは、フォーマット、ロギング、およびインターフェースベースのAPIで最もよく発生します。これらのAPIでは、値が処理される前にinterface{}(any)にボックス化されます。
func logValue(v int) {
fmt.Println(v) // vはインターフェースとして渡され、エスケープする可能性があります
}
ただし、インターフェースの使用が自動的にヒープ割り当てを引き起こすわけではありません。多くのインターフェース呼び出しはまったく割り当てを行わず、コンパイラはこの点でますます改善されています。インターフェースを完全に避けるのではなく、確認すべきものとして扱ってください。
スライス、マップ、構造体
値が現在の関数よりも長く存続するデータ構造内に格納されている場合、エスケープする可能性があります。マップ、スライス、または構造体フィールドにポインタを格納し、そのコンテナが関数よりも長く存続する場合、格納された値も同様に長く存続する必要があります。
type Cache struct {
items map[string]*Item
}
func (c *Cache) Add(key string, it *Item) {
c.items[key] = it // itはエスケープします: 長命な構造体に格納されます
}
コンテナとその保持する値の関係は、ここで重要です。関数から決して出ないスライスは、その内容をスタック上に保持する可能性があります。呼び出し元に返されたり、長命な構造体に格納されたりする同じスライスは、その内容をヒープにプッシュします。
Goでエスケープ解析を確認する方法
良いニュースは、一般的なエスケープの原因を覚えておく必要も、特定の値が特定の瞬間にエスケープしたかどうかを推測する必要もないことです。Goコンパイラのフラグがそれを教えてくれます。実際、コンパイラの出力を検査することが、何が起こっているかを確実に知る唯一の方法です。ログはエスケープ以上のものもカバーしています。割り当ての決定と並んで、コンパイラはインライニングの詳細やその他の診断情報を報告するため、特定のビルドに対して行われた最適化の決定のかなり完全な画像が得られます。欠点は、出力が正確にユーザーフレンドリーでナビゲートしやすいわけではないことですが、後でそれに戻ります。
コンパイラフラグの使用方法
Goコンパイラは、-gcflags debugフラグと-mオプションを使用してエスケープ解析情報を提供します:
go build -gcflags="-m" ./...
-mフラグは、コンパイラに最適化の決定(値がエスケープしたかどうかを含む)を出力するように要求します。出力はおおよそ次のようになります:
./user.go:6:2: moved to heap: u
./user.go:7:9: &u escapes to heap
より詳細な理由のために-mを2回(-gcflags="-m -m")渡すことができますが、それはすぐに...