🗂

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

2 min read

調べようと思ったきっかけは、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 では特にスタックかヒープかというのを特に意識しなくても問題はないようです。

Discussion

ログインするとコメントできます