【Go言語】Goのメモリ管理の全貌を徹底解剖

## はじめに
Goの学習を進めていく中で、普段あまりメモリ管理に気を使わずに学習を進めていました。
ただ、Goのコーディングのしやすさの要素の一つとして、「**メモリ管理**」が関わっているのではないかと思い、今回記事にまとめることにしました。



## メモリ管理が全てであるということ
現代のプログラミングにおいてCPUがボトルネックになることはほとんどありません。
「メモリI/O、ネットワークI/O、ディスクI/O」などのI/Oによってプロップされます。

Goのコンパイラーとランタイムは賢いメモリ管理をしているので、むやみにメモリ管理を改善しようとすると失敗してしまいます。

では、メモリ管理を理解するためには、まずOSの仕組みを理解する必要があります。Linux上でプログラムがどのように動作しているのかを確認していきましょう。

## OSの仕組み
こちらは軽く触れようと思います。[(こちらで深掘っています)](https://qiita.com/fujifuji1414/items/5373f3da51465c82d0d4)
まず、Linux上でプログラムがどのように動いているのか確認していきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763692/ab2a3dd1-184d-bfff-0807-614667a39947.png)

上図からもわかるように、プログラムが直接物理的な記憶媒体にアクセスすることはできません。プロセスはOSを経由することでのみ記憶媒体にアクセスすることが可能です。
したがって、プロセスはOSから提供される仮想メモリという仮想的な空間を使用します。
上図に細かく記載してありますが、最も伝えたいことは以下の点です。

:::note
**プロセスからは仮想メモリしか見えないということ**
:::

### 仮想メモリ
先ほども触れましたが、プロセスからは仮想メモリしかさわれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763692/c4b697e1-2ff0-9f44-4079-f023b43b5d03.png)


 ここで意識することは青色で示されている「**ヒープ**」と「**スタック**」です。
Goのランタイムとプロセスの「**ヒープ**」および「**スタック**」の関係性を知っていく必要があります。

### OSとの対応
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763692/4bc96053-76bf-45af-7443-bab6d78a4724.png)

プロセスの中の「**ヒープ**」というのが、Goランタイムのグローバルなオブジェクトである「**mheap**」に相当します。
また、「**スタック**」に関してですが、goroutineのスタックは`2KB`でとても小さいです。
OSのデフォルトサイズ(`8MB`)と比較してもかなり小さいことがわかるかと思います。
そうなると無駄が生じてしまうので、Goのランタイムは一つ一つのgoroutineにアサインされた細かいスタックをいい具合にOSスレッドのスタックに格納しています。

大体プログラムを書いていると「**ヒープ**」が問題であることが多いです。
GC(ガベージコレクション[^1])に関してもヒープを処理するものです。
そのため、ヒープを理解していくことがとても重要になってきます。
## Goの内部のメモリ構造
以下の画像はGoの内部のメモリ管理の概要となっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763692/cd9e40b9-68cf-b229-71c0-f953c8b20cae.png)

まず、Goランタイム全体で共有されている「**mheap**」があります。
そして、このmheapの中に「**arena**」という単位でメモリを確保しています。
さらに、arenaの中に「**mcentral**」というバケットがあります。

それでは、各要素を順に見ていきましょう。
### mheap（ページヒープ）
Goが動的データ（コンパイル時にサイズが計算できないデータ）を保持する場所です。
最大のメモリブロックであり、GCが行われます。

> **mspan**
- **mspan**はmheap内でメモリのページを管理する最も基本的な構造です
- これは「開始ページのアドレス」、「スパンサイズクラス」、「スパン内のページ数」を保持する*双方向連結リスト*です
- 各スパンは、ポインタ（`scan`クラス）を持つオブジェクト、もう一つはポインタを持たない（`noscan`クラス）オブジェクトの2種類が存在します
    - これはGCの際に、残っているオブジェクトを探すためにnoscanスパンを通過する必要がないため役に立ちます

メモリのアローケートは高コストな操作であるため、Goのランタイムは効率的なメモリ管理を行っています。具体的には、事前にメモリを確保しておき、その後必要に応じて使い回すという戦略を取っています。
**mspan**の中には、小さな*span*というオブジェクトがたくさんあり、それらがクラスごとに分かれてmcentralに保存されます。


### mcentral
**mcentral**は、同じサイズのクラスのスパンをまとめてグループ化します。
各mcentralは2つのmspanListを含みます

> **empty**
- 空きオブジェクトを持たないスパンや、mcacheにキャッシュされているスパンの双方向連結リスト
    - ここでスパンが解放されると、`non empty`リストに移動します
> **non empty**
- 空きオブジェクトを持つスパンの双方向連結リスト
- **mcentral**から新しいスパンが要求されると、`non empty`リストから`emtpy`リストに移動します

**mcentral**に空きスパンが存在しない場合、mheapから新しいページを要求します。

### arena
mheapの中では「**arena**」という単位でメモリを確保しています。
ヒープメモリは、割り当てられた仮想メモリの範囲内で必要に応じて増減します。
より多くのメモリが必要になると、mheapは**arena**と呼ばれる64MB（64ビットアーキテクチャの場合）のチャンク[^2]として仮想メモリから引き出します。
ここではページがスパンにマッピングされます。

### mcache
**mcache**は、P（論理プロセッサ）に提供されるメモリのキャッシュであり、小さなオブジェクト（サイズが32KB以下）を格納します。
これはスレッドスタックに似ていますが、ヒープの一部として動的データのために使用されます。
**mcache**には、全てのクラスサイズに対応したscanタイプとnoscanタイプのmspanが含まれています。

Pは同時に一つのgoroutineしか保持できないため、goroutineはロックされることなくmcacheからメモリを確保することができ、非常に効率的です。mcacheは必要に応じてmcentralから新しいスパンを要求します。

### stack
スタックメモリ領域でgoroutine毎に1つのスタックが存在します。この領域には関数フレーム、静的構造体、プリミティブ値、そして動的な構造体を保持するためのポインタを含む静的データが保持されています。
これはPに割り当てられる`mcache`とは異なります。

では、どのようにGoではオブジェクトを割り当てているのか見ていきましょう。
## スタックとヒープ
Golangは内部で自動的にメモリの割り当てを行います。
ローカル変数として宣言された変数が、他の関数に渡されたり返り値として使用される場合は、その変数の寿命が長くなるため、ヒープに割り当てられることがあります。

基本的には、高速性を優先してスタックに割り当てられますが、外部の関数や返り値として渡される場合は、ヒープに移されます。
以下のテーブルのようにまとめられます。
|       | **ヒープ**                          | **スタック**                         |
|------------------|----------------------------------|--------------------------------|
| **スコープ**         | レキシカルスコープ[^3]外                     | レキシカルスコープ[^3]内                   |
| **用途**            | グローバル変数・巨大なデータ                 | ローカル変数・関数の引数と戻り値            |
| **ポインターの有無**     | ポインター使用                           | ポインターでない                     |
| **ライフサイクル**       | 予測不能                               | LIFO（Last In, First Out）          |

スタックのライフサイクルは、データをすぐに入れてすぐに取り出すことができます。また、レキシカルスコープ内で完結しているため、ガベージコレクション（GC）が不要です。そのため、コンパイル時間でメモリの処理がわかっているため、速くなります。
一方で、ヒープはいつまでオブジェクトが参照されるのかがわからないので、GCなどを使用して消してあげる必要があります。

以上の点より、スタックでいいならスタックを使いましょう。

例えば以下のコードの変数`s`はヒープとスタックのどちらにあるでしょう。
```golang:main.go
package main

import (
	"fmt"
)

type Rectangle struct {
	Width, Height int
}

func main() {
	for i := 0; i < 100; i++ {
		s := NewRectangle(i, 2*i)
		fmt.Println(s.Area())
	}
}

func NewRectangle(w, h int) Rectangle {
	return Rectangle{
		Width:  w,
		Height: h,
	}
}

func (r Rectangle) Area() int {
	return r.Width * r.Height
}
```
Goでは、`-gcflags -m`を渡すことで、メモリがスタックとヒープどちらに確保されているかを知ることができます。
では、以下のコマンドを叩いてみましょう。
```terminal
$ go build -gcflags -m main.go

// 出力結果
./main.go:18:6: can inline NewRectangle
./main.go:25:6: can inline Rectangle.Area
./main.go:13:20: inlining call to NewRectangle
./main.go:14:21: inlining call to Rectangle.Area
./main.go:14:14: inlining call to fmt.Println
<autogenerated>:1: inlining call to Rectangle.Area
./main.go:14:14: ... argument does not escape
./main.go:14:21: ~r0 escapes to heap
```
出力を見てみると様々な関数がインライン化されています。
`NewRectangle`関数は値を返しています。そのため`does not escape`と出ます。
また、最後の行を見てみると`./main.go:14:21: ~r0 escapes to heap`でヒープに置かれていることが確認できます。

では、`NewRectangle`関数をポインタを返すように変えてみましょう。
```golang:main.go
func main() {
	for i := 0; i < 100; i++ {
		s := NewRectangle(i, 2*i)
		fmt.Println(s.Area())
	}
}

// ポインタを返すようにする
func NewRectangle(w, h int) *Rectangle {
	return &Rectangle{
		Width:  w,
		Height: h,
	}
}

func (r Rectangle) Area() int {
	return r.Width * r.Height
}
```
さっきのテーブルに記載していた通り、レキシカルスコープ外に行っているようなものであれば、ヒープに割り当てられるのではないかと感じるかもしれませんが、出力結果をみてみると、、、、
```golang
./main.go:18:6: can inline NewRectangle
./main.go:25:6: can inline Rectangle.Area
./main.go:13:20: inlining call to NewRectangle
./main.go:14:21: inlining call to Rectangle.Area
./main.go:14:14: inlining call to fmt.Println
// インライン展開されたものはスタックにあります
<autogenerated>:1: inlining call to Rectangle.Area
./main.go:13:20: &Rectangle{...} does not escape
./main.go:14:14: ... argument does not escape
./main.go:14:21: ~r0 escapes to heap
// 戻り値はヒープに渡されます
./main.go:19:9: &Rectangle{...} escapes to heap
```
`./main.go:13:20: &Rectangle{...} does not escape`をみるとインライン展開[^4]されているのでスタックに渡されます。

`does not escape`
- 変数 `&Rectangle{...}` は関数のスコープ外にエスケープしないことを示しています
- つまり、スタックに割り当てられていると言える

`escapes to heap`
- 変数 `&Rectangle{...}` や `~r0` はヒープにエスケープすることを示しています
- これは、これらの変数が関数のスコープ外で必要とされるため、ヒープメモリに割り当てられます

Goのコンパイラは、効率的なメモリ管理を実現するためにさまざまな最適化を行なっていることが確認できました。
Goのコンパイラは賢いですね〜。

## プリミティブ型のサイズ
データ型のサイズは以下の通りです。
| **データ型**       | **サイズ**     |
|--------------------|----------------|
| `bool`             | 1バイト        |
| `int`              | 8バイト（64ビット） |
| `int8`             | 1バイト        |
| `int16`            | 2バイト        |
| `int32`            | 4バイト        |
| `int64`            | 8バイト        |
| `uint`             | 8バイト（64ビット） |
| `uint8` (`byte`)   | 1バイト        |
| `uint16`           | 2バイト        |
| `uint32`           | 4バイト        |
| `uint64`           | 8バイト        |
| `uintptr`          | 8バイト        |
| `float32`          | 4バイト        |
| `float64`          | 8バイト        |
| `complex64`        | 8バイト（`float32`が2つ） |
| `complex128`       | 16バイト（`float64`が2つ） |
| `string`           | ポインタ＋長さに応じて変動 |

[A Tour of Go](https://go-tour-jp.appspot.com/basics/11)
ここで気をつけるべき点は、`string`型、`slice`型、`map`型についてです。
これらは、裏で配列のようなものを持っており、そこに対するポインターを持つ構造体というのがメインな部分となっています。
このため、`string`や`map`, `slice`のポインタを渡す場合の意味をよく理解する必要があります。
つまり、ポインターはアドレスの値を持った数値でしかないので、ポインターのポインターをとったところで、二度手間になっているだけなんですね。
さらに、ポインターにするとヒープに割り当てられる可能性が高くなるので処理が遅くなることが考えられます。

:::note
**必要以上にポインターを渡さない方が良い！**
:::

では、以下の構造体はどのくらいのメモリが確保されるでしょうか？
```golang
type Example struct {
	A uint32 // 4byte
	B uint16 // 2byte
}
```
[The Go Playground](https://go.dev/play/p/ufWAPmQUhvI)：実行してみましょう。
上のテーブルより、「6byteだ！」と思うかもしれませんが実は、**8byte**になります。なぜなら、「**メモリアラインメント**」があるからです。

## メモリアラインメント
**メモリアラインメント**
- メモリを効率的に使用するために、CPUやGoランタイムが決まったサイズのメモリを取得する仕組みです
- データが特定の境界に揃えられることで、CPUのアクセス速度が向上し、全体的なパフォーマンスが向上します

以下のようなイメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763692/d389d190-60e1-9779-2e39-1dbd21c31f06.png)

余った部分は**パディング**と言って、「0」で埋めてくれます。

このメモリアライメントの特徴として、*フィールドの並びによってサイズが変わるということ*です。
```golang
type Example struct {
	C bool
	B uint32
	A uint16
}
```
[The Go Playground](https://go.dev/play/p/Fs5B8UacSZa)：実行してみましょう
`12byte`と出力されますね
```golang
type Example struct {
    A uint32 
    B uint16 
	C bool
}
```
[The Go Playground](https://go.dev/play/p/FHNU_sNUstC)：実行してみましょう
`8byte`と出力されました。
どうなっているのか画像と一緒に見てみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763692/0f4202d0-1817-b579-4ab4-2a078d6bbaa2.png)
チューニングする際は、こういったところで多少のチューニングができそうですね。

## ガベージコレクション(GC)
Goはガベージコレクションによってヒープメモリを管理しています。
つまり、参照されていないオブジェクトが使用していたメモリを解放して、新しいオブジェクトを作成するためのスペースを確保します。
- 頻度が低いとOSからのメモリ確保の頻度が上がる
- 頻度が高いとプログラムが止められずぎて遅くなる

そのためGCには以下のような仕組みがあります。

- **Mark Setup(Stop the world)**
    - GCが始まると、コレクタは書き込みバリアをオンにし、次の並行フェーズでも整合性が保たれるようにします
    - このステップでは、実行中の全てのgoroutineが一時的に停止され、その後続行されます。ごくわずかな一時停止が必要です
- **Marking(Concurrent)**
    - 書き込みバリアがオンになると、実際のマーキング処理がアプリケーションと並列して開始され、使用可能なCPUの25%が使用されます。
    - マーキングが完了するまで、対応したPは占有されています
    - これは専用のgoroutineを使って行われます。ここでGCは稼働中（アクティブなgoroutineのスタックから参照されている）ヒープ上の値をマークします
    - 収集に時間がかかる場合は、アプリケーションのアクティブなgoroutineを使用してマーキングプロセスを支援します(=`マーク支援`)
- **Mark Termination(Stop the world)**
    - マーキングが完了すると、すべてのアクティブなgoroutineは一時停止し、書き込みバリアがオフになり、クリーンアップ作業が開始する。また、GCはここで次のGCのゴールを計算します
    - これが完了すると、確保されたPは解放されアプリケーションに戻されます
- **Sweeping(Concurrent)**
    - 収集が完了し、割り当てが試みられると、スウィーピングプロセスは、未活動とマークされたヒープからメモリを取り戻し始めます
    - メモリの吐き出し量は、割り当てられている量と同期しています

多くの言語ではマークアンドスイープアルゴリズムが使われているようで、Golangでも採用されています。

## さいごに
今回は、Goにおけるメモリ管理についてまとめてみました。具体的には、スタックとヒープの違いや、ガベージコレクションの仕組みとその影響について学べました。
内部構造を知ることで、Goの様々な工夫に触れることができましたね。
今後も、Goの深い理解と実践的な応用を追求していこうと思います！！




## 参考
https://zenn.dev/kazu1029/articles/38ab3d99ef0de3#go%E3%81%AE%E3%83%A1%E3%83%A2%E3%83%AA%E7%AE%A1%E7%90%86

https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html

https://speakerdeck.com/deepu105/go-gc-visualized

[^1]:メモリ上の不要なデータを自動的に削除する仕組み
[^2]: 大きなデータやメモリを扱う際に使用される単位
[^3]:変数がどこで宣言され、どこで使えるかがソースコードの記述によって定まる仕組み
[^4]:小さな関数やメソッドをその場で展開してパフォーマンスを向上させる技術