ガベージ コレクションは Go の非常に便利な機能です。自動メモリ管理によりコードがクリーンになり、可能性も減ります。メモリリークのこと。ただし、ガベージ コレクションでは、未使用のオブジェクトを収集するためにプログラムを定期的に停止する必要があるため、オーバーヘッドが必然的に追加されます。 Go コンパイラは賢く、将来の収集を容易にするために変数をヒープに割り当てるべきか、関数のスタック領域に直接割り当てるべきかを自動的に判断します。スタック上に割り当てられた変数の場合、ヒープ上に割り当てられた変数との違いは、関数が戻るときにスタック領域が破棄されるため、スタック上の変数は追加のガベージ コレクション オーバーヘッドなしで直接破棄されることです。
Go のエスケープ分析は、Java 仮想マシンの HotSpot よりも基本的なものです。基本的なルールは、変数への参照が、その変数が宣言されている関数から返された場合、「エスケープ」が発生することです。変数は関数の外の他のコンテンツによって使用される可能性があるため、ヒープ上に割り当てる必要があります。次の状況はさらに複雑になります。
- 他の関数を呼び出す関数
- メンバ変数を構造体として参照
- スライスとマッピング
- Cgo へのポインタ変数
エスケープ分析を実装するために、Go はコンパイル段階で入力パラメーターと戻り値のプロセスを追跡しながら関数呼び出しグラフを構築します。関数がパラメーターを参照するだけで、その参照が関数を返さない場合、変数はエスケープされません。関数が参照を返したにもかかわらず、その参照がスタック上の別の関数によって解放された場合、または参照を返さなかった場合、回避方法はありません。いくつかの例を示すために、コンパイル中に -gcflags '-m'
パラメーターを追加できます。このパラメーターは、エスケープ分析の詳細情報を出力します:
package main type S struct {} func main() { var x S _ = identity(x) } func identity(x S) S { return x }
## を実行できます。 #go run -gcflags '-m -l' (注: go コード ファイル名は原文では省略されています) このコードをコンパイルします。 -l パラメーターは、関数
identity がインライン化されるのを防ぎます(このトピックについては、インライン化については別の機会に説明します)。出力は表示されません。 Go は値転送を使用するため、
main 関数の #xxx
変数は常に関数 identity
のスタック領域にコピーされます。通常、参照を使用しないコードはスタック スペースを通じてメモリを割り当てます。したがって、エスケープ分析は含まれません。より難しいものを試してみましょう: <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">package main
type S struct {}
func main() {
var x S
y := &x
_ = *identity(y)
}
func identity(z *S) *S {
return z
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
対応する出力は次のとおりです:
./escape.go:11: leaking param: z to result ~r1 ./escape.go:7: main &x does not escape
最初の行は変数
z の「フロー」を示しています: 入力パラメータは直接として使用され、戻り値が返されます。ただし、関数 identity
は参照 z
を削除しなかったため、変数エスケープは発生しませんでした。 main
関数が戻った後は、x
への参照が存在しないため、x
変数には main
関数のスタック領域にメモリを割り当てることができます。 。 3 番目の実験:
package main type S struct {} func main() { var x S _ = *ref(x) } func ref(z S) *S { return &z }
出力は次のとおりです:
./escape.go:10: moved to heap: z ./escape.go:11: &z escapes to heap
これで脱出が可能になりました。 Go は値渡しであるため、
z は変数 x
のコピーであることに注意してください。関数 ref
は z
への参照を返すため、z
をスタックに割り当てることはできません。そうでない場合は、関数 ref
が返されるときに参照が割り当てられます。それはどこを指しているのでしょうか?それでそれは山に逃げました。実際、ref
を実行して main
関数に戻った後、main
関数は参照を逆参照するのではなく参照を破棄しますが、Go のエスケープ分析は十分にスマートではありません。それを特定するために。 この場合、参照を停止しないとコンパイラは
ref
をインライン化することに注意してください。 構造体のメンバーが参照として定義されている場合はどうなりますか?
package main type S struct { M *int } func main() { var i int refStruct(i) } func refStruct(y int) (z S) { z.M = &y return z }
出力は次のとおりです:
./escape.go:12: moved to heap: y ./escape.go:13: &y escapes to heap
この場合、参照が構造体のメンバーであっても、Go は参照のフローを追跡します。関数
refStruct は参照を受け入れて返すため、y
はエスケープする必要があります。次の例を比較してください: <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">package main
type S struct {
M *int
}
func main() {
var i int
refStruct(&i)
}
func refStruct(y *int) (z S) {
z.M = y
return z
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
出力は次のようになります:
./escape.go:12: leaking param: y to result z ./escape.go:9: main &i does not escape
変数は main
関数で参照され、関数 # に渡されます。 ##refStruct ただし、この参照のスコープは、宣言されているスタック領域を超えません。これは前のプログラムとはセマンティクスが若干異なり、より効率的になります。前のプログラムでは、変数
i を
main 関数のスタックに割り当てる必要があり、それをパラメータとして使用します。これを関数
refStruct にコピーし、コピーしたコピーをヒープに割り当てます。この例では、
i は 1 回だけ割り当てられ、その後参照が渡されます。
少し複雑な例を見てみましょう:
package main type S struct { M *int } func main() { var x S var i int ref(&i, &x) } func ref(y *int, z *S) { z.M = y }
./escape.go:13: leaking param: y ./escape.go:13: ref z does not escape ./escape.go:9: moved to heap: i ./escape.go:10: &i escapes to heap ./escape.go:10: main &x does not escape
问题在于,y
被赋值给了一个入参结构体的成员。Go并不能追溯这种关系(go只能追溯输入直接流向输出),所以逃逸分析失败了,所以变量只能分配到堆上。由于Go的逃逸分析的局限性,许多变量会被分配到堆上,请参考此链接,这里面记录了许多案例(从Go1.5开始)。
最后,来看下映射和切片是怎样的呢?请记住,切片和映射实际上只是具有指向堆内存的指针的Go结构:slice
结构是暴露在reflect
包中(SliceHeader
)。map
结构就更隐蔽了:存在于hmap。如果这些结构体不逃逸,将会被分配到栈上,但是其底层的数组或者哈希桶中的实际数据会被分配到堆上去。避免这种情况的唯一方法是分配一个固定大小的数组(例如[10000]int
)。
如果你剖析过你的程序堆使用情况(https://blog.golang.org/pprof
),并且想减少垃圾回收的消耗,可以将频繁分配到堆上的变量移到栈上,可能会有较好的效果。进一步研究HotSpot JVM是如何进行逃逸分析的会是一个不错的话题,可以参考这个链接,这个里面主要讲解了栈分配,以及有关何时可以消除同步的检测。
更多golang相关技术文章,请访问golang教程栏目!