Edited at

golangではスタックとヒープを気にする必要が無い

More than 1 year has passed since last update.

調べようと思ったきっかけは、golangでは以下のように

ローカル変数のアドレスを戻り値としても問題ないということ。

package main

import (
"fmt"
)

type Animal struct {
Name string
Age int
}

func main() {
animal := allocAnimal()
fmt.Printf("allocate animal structure %p", animal)
}

func allocAnimal() *Animal {
return &Animal{}
}

ポインタを扱えるC/C++ではローカル変数のポインタを戻り値とした場合、スタック領域のポインタを関数外に渡すため、

コンパイル時点で警告が表示されます。(なぜエラーにしない)

実行時には最悪、セグメンテーションフォールトで落ちます。

そのため、mallocやnewでヒープ領域にメモリを割り当てる必要があります

なぜ、golangではこれが許されるのか気になり調べてみました


スタックとヒープについて

golangにおけるスタックとヒープについて簡単にですが、説明します。


スタック


  • 確保・解放が早い

  • 寿命が短い。関数内もしくは特定のスコープ内限定

  • サイズが小さい

  • ローカル変数・引数などで使われる


ヒープ


  • 確保・解放が遅い

  • 寿命は自由

  • サイズが大きい

  • newなどで確保される


golangのFAQ

golangのFAQにはスタックとヒープの扱いでは以下のように記載されています。

※全文はリンク先にて参照ください

[http://golang.jp/go_faq#stack_or_heap:title]


正確さを期するなら知る必要はありません。Goの各変数は参照されている限り存在し続けます。Goの実装によって選択された格納場所がどこかは、Go言語的には意味を持ちません。


と、どうやらコンパイラが適宜判断して、関数内に収まる場合はスタックに、関数外でも参照される変数はローカル変数でもヒープに割り当てられるようです。


実際に調べてみた

コンパイラにフラグを渡すと、メモリ割当の様子がわかります。


go build -gcflags -m hello.go


サンプルコードそのままの様子を見てみます。

$ go build -gcflags -m main.go

./main.go:17: can inline allocAnimal
./main.go:13: inlining call to allocAnimal
./main.go:14: animal escapes to heap
./main.go:13: &Animal literal escapes to heap
./main.go:14: main ... argument does not escape
./main.go:18: &Animal literal escapes to heap // ローカル変数がヒープに

コメントの箇所でコンパイラが関数の外で扱われると判断し、スタックではなくヒープに領域を確保しています。


newで確保したメモリを関数の外に出さない場合

では、コードを以下のようにするとどうでしょうか。

func allocAnimal() *Animal {

animal := new(Animal)
animal.Name = "Cat"
animal.Age = 23
return &Animal{}
}

このコードの内容にはあまり意味はありませんが、newしたものを関数内で完結させています。

このコードの様子を再度出力してみます。

./main.go:17: can inline allocAnimal

./main.go:13: inlining call to allocAnimal
./main.go:14: animal escapes to heap
./main.go:13: &Animal literal escapes to heap
./main.go:13: main new(Animal) does not escape // newしてもヒープに割り当てられない
./main.go:14: main ... argument does not escape
./main.go:22: &Animal literal escapes to heap
./main.go:18: allocAnimal new(Animal) does not escape

golangの場合はnewで確保しても関数内で完結するものはヒープではなくスタックに割り当てられるようです。

このように、golangでは特にスタックかヒープかというのを特に意識しなくても問題はないようです。

長く、C/C++をやっていた人間からするとモヤモヤしますが。。。