🤯

goに入門したのでgcのDOCを読んだメモ

2024/03/16に公開

元のドキュメント

Goに入門したのでこちらを読んでます。
GoのGCについて解説した公式DOCです。
https://go.dev/doc/gc-guide#Implementation-specific_optimizations
(努力はしてますが)認識違いがあるかもしれないのでこの記事の内容を間に受けないようにお願い致します。

メモ本編

前提

Goの1.19時点の話。

This document currently describes the garbage collector as of Go 1.19.

コンパイル時の動作

Goのコンパイラがコンパイルする際

  • いつ解放して良いか決められる箇所はスタックに割り当て(厳密にはgo routine stack?)
  • いつ解放できるかわからない時はヒープに割り当て

GoのGCの動きの雰囲気

GoのGCはトレーシングガベージコレクションとのこと。
ポインタを推移的に追っていき、生存しているオブジェクトを追っていく。

  • オブジェクト:1つ以上のGoの値を保持している動的メモリのカケラ(Pieceをどう訳すか悩んだ)
  • ポインタ:オブジェクトの中のあらゆる値のメモリ上のアドレス

プログラムによって確実に利用されている値(例えばローカル変数やグローバル変数らしい)をルートと呼ぶことにする。GCはルートからオブジェクトを辿りグラフを作成していく。
このグラフはオブジェクトと他のオブジェクトへのポインタを持つ。

GoのGCはマークスウィープという方式を利用する。

  1. GC実行に出会った値を「live」としてマーク
  2. ヒープメモリ全体をスキャンし、マークされてない箇所を割り当て可能とする

GoのGCの実行タイミング

GoではユーザーがGCの実行タイミングを「GOGC」なるもので制御できる。

  • ターゲットヒープサイズ:liveヒープ+(liveヒープ+GCルーツ)×GOGC/100
  • トータルのヒープサイズ:前回のGC時のヒープサイズ + 新規GCサイクルでのヒープサイズ

GOGCは各GCサイクルの後、次のサイクルでのターゲットヒープサイズを決定する
GCはトータルのヒープサイズがターゲットヒープサイズを超える前にコレクションサイクルを終了させることを目標に動く
(この辺りは理解がふんわり)

GOGCの値を変更することでGCの実行頻度を変更できる。

GOGCの値をどうするか? GC実行頻度 ヒープサイズ CPU使用率
今より大きくする 今より減る 増える 下がる
今より小さくする 今より増える 減る 上がる

正直、実際のドキュメントにある図を操作して体感するのが一番良い。

GoのGCのメモリに関する設定

ただ、ヒープサイズがメモリ上限が明確にある場所で動く場合、問題がある。(コンテナとか)
なのでGOMEMLIMITという環境変数を設定。
ただしこれは「ソフト」に守られるらしい。

  • コンテナで動かすならメモリ制限は有用
  • コンテナで設定する際には、経験則的に、上限の5-10%に設定すると良い
  • 例えば100MiBであれば90MiBから95MiB。

最適化ガイドの中身をメモ

  • リーフ関数:他の関数を呼び出さない、それ単体の関数
  • 非リーフ関数:他の関数を呼び出す関数
  1. CPUプロファイル
    基本はpprofだが、非リーフ関数は追えないのでtop -cumを利用して「各関数の累積的な実行時間」を確認する
    listコマンドに関数名を指定するとその関数の詳細なプロファイル情報が表示される
  • runtime.gcBgMarkWorker :GoのGCのマークワーカープロセスのへのエントリーポイント。GC頻度、オブジェクトグラフの複雑性とサイズに依存して大きくなる。
  • runtime.mallocgc:ヒープメモリのアロケーターへのエントリーポイント。
  • runtime.gcAssistAlloc:5%を超えたらアプリケーションがGCのマーキングとスキャンに時間を費やしていることを推察できる(らしい)
  1. 実行トレース
    プロファイリングは詳細を提供しないのでruntime/traceのパッケージを利用しよう!と書いてあった。
    https://pkg.go.dev/runtime/trace

  2. Goトレース
    GODEBUG=gctrace=1 を有効にしてGCのトレースができる
    GODEBUG=gcpacertrace=1 でペーサートレースというものがある
    → これ読んでね!らしい。
    https://go.dev/doc/gc-guide#Additional_resources

ヒープメモリ

pprofを利用すればホットスポットは確認できるとのこと。
各メモリプロファイルは下記のようにメモリを分類するらしい。

inuse_objects—Breaks down the number of objects that are live.
inuse_space—Breaks down live objects by how much memory they use in bytes.
alloc_objects—Breaks down the number of objects that have been allocated since the Go program began executing.
alloc_space—Breaks down the total amount of memory allocated since the Go program began executing.

以下google翻訳に通したもの

inuse_objects - ライブ状態のオブジェクトの数の内訳。
inuse_space - ライブ オブジェクトを、使用するメモリ量 (バイト単位) で分類します。
alloc_objects - Go プログラムの実行開始以降に割り当てられたオブジェクトの数の内訳。
alloc_space - Go プログラムの実行開始以降に割り当てられたメモリの合計量の内訳

どの状態で見たいかをフラグ形式で指定すれば良いとのこと。

エスケープ分析

ヒープメモリの分析をした後、どう改善するか?という話。
下記コマンドで指定するのが一番簡単らしい。

go build -gcflags=-m=3 [package]

同様のことはVSCodeのプラグインでもできるとのこと。
GoのVSCodeのプラグインを入れた状態で、ドキュメントの通りsettings.jsonに下記のように設定するとなんか生えてきた。

"gopls": {
            "codelenses": {
                "generate": true,  // Don't show the `go generate` lens.
                "gc_details": true  // Show a code lens toggling the display of gc's choices.
            },
            "diagnostic.annotations": {
                "escape": true,
            }
        }

こんな感じ

Implementation-specific optimizations → 飛ばした。

互換性がなくなるから本当に必要な時にね!みたいな感じだったので気が進まず真面目に読まなかった。

Linux transparent huge pages (THP)

手順としては

  1. /sys/kernel/mm/transparent_hugepage/enabled を有効にする
  2. /sys/kernel/mm/transparent_hugepage/defrag を defer または defer+madvise に設定

THP:透過的ヒュージページとでも訳しておく。仮想メモリのバックアップをする物理メモリのページをヒュージページと呼ばれるブロックに透過的に置き換えるということらしい。

THPを有効にする際、1GiB以上のヒープメモリを利用する場合は最大10%程度のスループットが得られることもあるとのこと。なので、頭の片隅に入れて実験してみよう!みたいな感じ。

(蛇足)この記事の存在意義

自分が後で見返しながら理解を深める用です。
知り合いとワイワイする際にこれをシェアしながら会話する時にも利用する予定です。

Discussion