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

「休憩中Gopherくん」 Powered by Gopherize.me - A Gopher pic that's as unique as youThe Gopher character is based on the Go mascot designed by Renée French

はじめに

こんにちは!!!!ライクル事業部エンジニアの黒田(@knkurokuro7)です。

画像のGopherくんみたいに休憩中はよくコーヒーを飲んでいます!笑
今日もGoについてブログを書こうと思います!

このブログを書こうとしたきっかけ

最近Twitterで見かけて、興味を持ったこの記事を読もうとしたのですが、知らない用語や概念が多すぎて、挫折しました。。。 go.dev

本来はこの記事の全体を踏まえてブログを書こうとしたのですが、無理でした。。。

そのため、この記事を理解した上で、まとめたブログを書くための一つのステップとして、序盤のWhere Go Values Live(スタックとヒープについて説明されている)の部分を深ぼってみることにしました。

目的

  • 上のGCについての記事にある「Where Go Values Live」の章に沿って、Goのスタックとヒープについてまとめます。
  • GCについては詳しく触れません。また別の機会に書きたいと思います。

用語の意味

まず、この章を読むにあたって、意味がよくわからない用語がたくさん出てきました。 そのため、まずは用語を整理します。 ただし、今回の文脈を踏まえての意味を整理することにします。

メモリ

  • CPUがデータを読み書きする際にデータを置いておく領域のこと。
  • プログラミング上で実行した値が、メモリ上のどこかに領域が確保される。

レキシカルスコープ

静的スコープについてはこちらを参照して理解しました。

ksmakoto.hatenadiary.com

このように、手続きオブジェクトのようなものを引張りまわしたとしても、ソースコード上で元々静的に確定できる範囲のものが見えるというスコープ規則が「静的スコープ」である。

まとめると、レキシカルスコープとは、

  • 静的スコープともいう。ローカルスコープとは異なる。
  • ソースコード上で元々静的に確定できる範囲のものが見えるというスコープ規則のこと。

ということらしいです。

GC

  • ガベージコレクションのこと。
  • ヒープに確保された、もう使われないと判断されたメモリを解放する。

アロケーション

  • プログラム実行時に、必要なメモリを確保することをメモリアロケーションという。
  • 「アロケーション」の訳として「割り当て」や「確保」とすることがある。

コンパイラ

  • コードを機械語に翻訳してくれるプログラムのこと。
  • Goコンパイラはコードを分析して、それをスタックかヒープのどちらに割り当てるべきか判断する。

ランタイム

  • プログラム実行時に必要となる部品のこと。
  • Goでは、メモリの割り当てやGCを動かす機能がruntimeパッケージに書かれている。

スタックとヒープについては、「Where Go Values Live」に説明があるので、省きます。

「Where Go Values Live」について

用語もあらかた把握したところで、「Where Go Values Live」の部分を翻訳しながら読んでみることにしました。
一部を抜粋して読んでいきます。

まずは、スタックのことについて書かれています。

For instance, non-pointer Go values stored in local variables will likely not be managed by the Go GC at all, and Go will instead arrange for memory to be allocated that's tied to the lexical scope in which it's created. In general, this is more efficient than relying on the GC, because the Go compiler is able to predetermine when that memory may be freed and emit machine instructions that clean up. Typically, we refer to allocating memory for Go values this way as "stack allocation," because the space is stored on the goroutine stack.

翻訳

例えば、ローカル変数に格納されたポインタではないGoの値は、GoのGC によって全く管理されない可能性が高く、代わりにGoはメモリが作成されたところのレキシカルスコープに結びついたメモリが割り当てられるように配置します。一般に、これはGCに依存するよりも効率的です。なぜなら、Goコンパイラは、メモリがいつ解放されるかを事前に決定し、クリーンアップする機械命令を出すことができるからです。一般に、この方法でGoの値のメモリを確保することを「スタックアロケーション」と呼びますが、これはその領域がgoroutineスタックに格納されるからです。

次にヒープのことについて触れられています。

Go values whose memory cannot be allocated this way, because the Go compiler cannot determine its lifetime, are said to escape to the heap. "The heap" can be thought of as a catch-all for memory allocation, for when Go values need to be placed somewhere. The act of allocating memory on the heap is typically referred to as "dynamic memory allocation" because both the compiler and the runtime can make very few assumptions as to how this memory is used and when it can be cleaned up. That's where a GC comes in: it's a system that specifically identifies and cleans up dynamic memory allocations.

There are many reasons why a Go value might need to escape to the heap. One reason could be that its size is dynamically determined. Consider for instance the backing array of a slice whose initial size is determined by a variable, rather than a constant. Note that escaping to the heap must also be transitive: if a reference to a Go value is written into another Go value that has already been determined to escape, that value must also escape.

翻訳

Goコンパイラがその寿命を決定できないため、この方法でメモリを確保できないGoの値は、ヒープにエスケープされると言われています。「ヒープ」は、Goの値をどこかに置く必要があるときのための、メモリアロケーションのすべてを捕らえるものと考えることができます。ヒープにメモリを割り当てる行為は、一般に「動的メモリアロケーション」と呼ばれます。これは、コンパイラとランタイムの両方が、このメモリがどのように使用され、いつクリーンアップされるかについて、ほとんど仮定できないからです。そこでGCが登場する場面です。GCは、動的なメモリアロケーションを明確に識別し、クリーンアップするシステムです。

Goの値がヒープにエスケープされる必要がある理由はたくさんあります。理由のひとつは、そのサイズが動的に決定されることです。例えば、スライスの元になる配列の初期サイズが定数ではなく、変数によって決定される場合を考えてみましょう。ヒープへのエスケープは推移的でなければならないことに注意してください。あるGoの値への参照が、エスケープされることがすでに決まっている別のGoの値に書き込まれると、その値もエスケープしなければなりません。

ここまでの文章で、スタックとヒープについて以下のことが読み取れました。

スタック

  • ローカル変数で、ポインタではないGoの値が割り当てられる。
  • GCされない。
  • GCを使わないので効率的。
  • レキシカルスコープに結びついたメモリが割り当てられる。
  • Goコンパイラが、メモリを確保する。
  • goroutineスタックに格納される。

ヒープ

  • Goコンパイラでメモリを確保できない値がヒープにエスケープされる。
  • GCされる。
  • Goコンパイラとランタイムによって、いつ使用され、いつクリーンアップされるか仮定できない。
  • サイズが動的に決定される。

スタックとヒープにそれぞれ割り当てられる場合

ここまでで、スタックとヒープというものがどのようなものか大体理解した気になれました。 そこで、実装をする上でどのように結びつくのかということについて調べました。 主にこちらの記事を参照しました。

segment.com

実装する上でのスタックとヒープ

  • 関数内でしか使われない値は基本的にスタックに割り当てられる。
  • 関数外で使われると判断された場合はヒープに割り当てられる。
  • ポインタは基本的にヒープに割り当てられるが、関数内でしか使われない場合はスタックに割り当てられる場合もある。
  • スライス文字列(バイト列に対するスライス)も、それぞれサイズが動的に決定されるので、ヒープに割り当てられる。
  • GCを使わないので(他にも理由はあるが)、基本的にスタックに割り当てられた方が速い処理になる。

これまで実装の中で何も考えずに関数の返り値をポインタにすることが多かったのですが、ポインタで返すべきかどうかを、他の要素(今回は触れません)も踏まえて吟味したいなと思います!

終わりに

スタックとヒープというものをGoを使っている中で意識したことはありませんでした。
ただ、スタックとヒープという概念を知ることをきっかけとして、どのようなコードを書くとより効率的なのかということを考えようと思いました!!

次はGoのGC自体について深ぼった記事を書いてみたいです!!

もしこの記事の内容についてご指摘等ございましたら、お気軽に教えていただけると嬉しいです!!

ここまで読んでいただいて本当にありがとうございます。

参考にさせていただいた記事

A Guide to the Go Garbage Collector - The Go Programming Language

Allocation efficiency in high-performance Go services | Twilio Segment Blog

プログラムがメモリをどう使うかを理解する(1)

アロケーション(アロケート)とは - 意味をわかりやすく - IT用語辞典 e-Words

静的スコープと動的スコープ・浅いアクセスと深いアクセス - ksmakotoのhatenadiary

プログラミング基礎 - Qiita

並行処理を支えるGoランタイム|Goでの並行処理を徹底解剖!

go gc algorithm 101 - Speaker Deck

GolangのGCを追う | Taichi Nakashima

GoのGCを10分で学ぼう - Qiita