Go言語入門:ガベージコレクター -Vol.27-

スポンサーリンク
Go言語入門:ガベージコレクター -Vol.27- ノウハウ
Go言語入門:ガベージコレクター -Vol.27-
この記事は約11分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のガベージコレクターついて解説しています。

スポンサーリンク

背景

Goのガベージコレクター(GC)は、多くの開発者にとって「ブラックボックス」のような存在です。メモリ管理を自動で行ってくれる便利な仕組みである一方、アプリケーションのパフォーマンスに大きな影響を与える要因でもあります。「なぜ突然レスポンスが遅くなるのか?」「メモリ使用量が想定より多いのはなぜか?」「GCの停止時間をもっと短くできないか?」—— こうした疑問は、Goで高性能なアプリケーションを開発する上で避けて通れない課題です。

本記事では、Go公式ドキュメントの「ガベージコレクションガイド」を日本語で紹介します。このガイドは、GCの動作原理を理解し、その知見を活用してアプリケーションのリソース使用効率を改善することを目的としています。特筆すべきは、このドキュメントがガベージコレクションの前提知識を一切要求しない点です。Go言語の基本的な知識さえあれば、誰でもGCの仕組みを深く理解できるよう設計されています。

なぜ今、GCの理解が重要なのでしょうか。クラウドネイティブ時代において、リソースの効率的な活用はコスト削減に直結します。また、マイクロサービスアーキテクチャでは、各サービスのレイテンシが全体のユーザー体験に影響するため、GCによる一時停止を最小限に抑えることが求められます。このガイドを通じて、「なんとなく動いている」から「理解して最適化できる」レベルへとステップアップし、より高品質なGoアプリケーションの開発を目指しましょう。

GOGCに関する追加注記

GOGCセクションでは、GOGCを2倍にするとヒープメモリオーバーヘッドが2倍になり、GC CPUコストが半分になると主張しました。その理由を数学的に分解して見てみましょう。

まず、ヒープターゲットは総ヒープサイズのターゲットを設定します。しかし、このターゲットは主に新規ヒープメモリに影響します。なぜなら、ライブヒープはアプリケーションにとって基本的なものだからです。

ターゲットヒープメモリ = ライブヒープ + (ライブヒープ + GCルート) * GOGC / 100
総ヒープメモリ = ライブヒープ + 新規ヒープメモリ
⇒
新規ヒープメモリ = (ライブヒープ + GCルート) * GOGC / 100

これから、GOGCを2倍にすると、アプリケーションが各サイクルで割り当てる新規ヒープメモリの量も2倍になることがわかります。これがヒープメモリオーバーヘッドを捉えています。なお、ライブヒープ + GCルートは、GCがスキャンする必要があるメモリ量の近似値です。

次に、GC CPUコストを見てみましょう。総コストは、サイクルごとのコストに、ある時間期間TにおけるGC頻度を掛けたものとして分解できます。

総GC CPUコスト = (サイクルごとのGC CPUコスト) * (GC頻度) * T

サイクルごとのGC CPUコストは、GCモデルから導出できます:

サイクルごとのGC CPUコスト = (ライブヒープ + GCルート) * (バイトあたりのコスト) + 固定コスト

なお、マークとスキャンのコストが支配的であるため、ここではスイープフェーズのコストは無視されています。

定常状態は、一定の割り当て率と一定のバイトあたりのコストによって定義されるため、定常状態では、この新規ヒープメモリからGC頻度を導出できます:

GC頻度 = (割り当て率) / (新規ヒープメモリ) = (割り当て率) / ((ライブヒープ + GCルート) * GOGC / 100)

これらをまとめると、総コストの完全な方程式が得られます:

総GC CPUコスト = (割り当て率) / ((ライブヒープ + GCルート) * GOGC / 100) * ((ライブヒープ + GCルート) * (バイトあたりのコスト) + 固定コスト) * T

十分に大きなヒープ(ほとんどのケースを表す)では、GCサイクルの限界コストが固定コストを支配します。これにより、総GC CPUコストの式を大幅に簡略化できます。

総GC CPUコスト = (割り当て率) / (GOGC / 100) * (バイトあたりのコスト) * T

この簡略化された式から、GOGCを2倍にすると、総GC CPUコストが半分になることがわかります。(なお、このガイドの視覚化は固定コストをシミュレートしているため、それらによって報告されるGC CPUオーバーヘッドは、GOGCが2倍になったときに正確に半分にはなりません。)さらに、GC CPUコストは主に割り当て率とメモリをスキャンするバイトあたりのコストによって決定されます。これらのコストを具体的に削減する方法の詳細については、最適化ガイドを参照してください。

注:ライブヒープのサイズと、GCが実際にスキャンする必要があるそのメモリの量との間には不一致が存在します。同じサイズのライブヒープでも、異なる構造では異なるCPUコストになりますが、同じメモリコストになるため、異なるトレードオフになります。これが、ヒープの構造が定常状態の定義の一部である理由です。ヒープターゲットは、GCがスキャンする必要があるメモリのより近い近似値として、スキャン可能なライブヒープのみを含めるべきですが、これは、スキャン可能なライブヒープが非常に少ないが、ライブヒープが他の点で大きい場合に、退化した動作につながります。

GOGCの数学的理解 – なぜ2倍で半分なのか?

数式を使って、GOGCの効果を正確に理解しましょう。

基本的な主張の復習

GOGCを2倍にすると:
✅ ヒープメモリオーバーヘッド: 2倍
✅ GC CPUコスト: 半分

本当にそうなるのか? 数式で確認しましょう。

ステップ1: ヒープメモリの分解

総ヒープメモリの構成

総ヒープメモリ = ライブヒープ + 新規ヒープメモリ
                  ↑           ↑
               必要なもの   オーバーヘッド

例え話:

家の広さ:
- ライブヒープ = 家具(必要なもの)
- 新規ヒープ = 空きスペース(余裕)
- 総ヒープ = 家全体の広さ

ターゲットヒープメモリの式

ターゲットヒープメモリ = ライブヒープ + (ライブヒープ + GCルート) × GOGC/100

具体例:

ライブヒープ: 10 MB
GCルート: 2 MB
GOGC: 100

ターゲット = 10 + (10 + 2) × 100/100
          = 10 + 12
          = 22 MB

新規ヒープメモリの導出

総ヒープメモリ = ライブヒープ + 新規ヒープメモリ
ターゲットヒープメモリ = 総ヒープメモリ

∴ 新規ヒープメモリ = (ライブヒープ + GCルート) × GOGC/100

これが重要!

新規ヒープメモリ ∝ GOGC

つまり:
GOGCを2倍 → 新規ヒープメモリも2倍

数値例で確認

前提:
ライブヒープ: 10 MB
GCルート: 2 MB

GOGC = 100:
新規ヒープ = (10 + 2) × 100/100 = 12 MB

GOGC = 200:
新規ヒープ = (10 + 2) × 200/100 = 24 MB
                                   ↑
                                 2倍!

視覚化:

GOGC = 100:
┌──────────┬────────────┐
│ライブ10MB│新規12MB    │
└──────────┴────────────┘
総: 22 MB

GOGC = 200:
┌──────────┬──────────────────────────┐
│ライブ10MB│新規24MB                  │
└──────────┴──────────────────────────┘
総: 34 MB (約1.5倍)

ステップ2: GC CPUコストの分解

総GC CPUコストの構成

総GC CPUコスト = サイクルごとのコスト × GC頻度 × 時間T

例え話:

掃除の総コスト:
= 1回の掃除時間 × 掃除回数 × 期間

サイクルごとのGC CPUコスト

サイクルごとのコスト = (ライブヒープ + GCルート) × バイト単価 + 固定コスト
                      ↑ スキャンするメモリ量

具体例:

ライブヒープ: 10 MB
GCルート: 2 MB
バイト単価: 10 ns/MB (スキャン速度)
固定コスト: 1 ms

サイクルごとのコスト = (10 + 2) × 10 ns + 1 ms
                     = 120 ns + 1 ms
                     ≈ 1 ms (固定コストが支配的)

GC頻度の導出

GC頻度 = 割り当て率 / 新規ヒープメモリ
       = 割り当て率 / ((ライブヒープ + GCルート) × GOGC/100)

具体例:

割り当て率: 100 MB/秒
ライブヒープ + GCルート: 12 MB

GOGC = 100:
GC頻度 = 100 / (12 × 100/100)
       = 100 / 12
       ≈ 8.3 回/秒

GOGC = 200:
GC頻度 = 100 / (12 × 200/100)
       = 100 / 24
       ≈ 4.2 回/秒
       ↑
     半分!

総GC CPUコストの完全な式

総コスト = (割り当て率 / ((ライブヒープ + GCルート) × GOGC/100))
         × ((ライブヒープ + GCルート) × バイト単価 + 固定コスト)
         × T

これは複雑! 簡略化しましょう。

ステップ3: 簡略化

仮定: 大きなヒープ

大きなヒープでは:
限界コスト >> 固定コスト

つまり:
(ライブヒープ + GCルート) × バイト単価 >> 固定コスト

∴ 固定コストを無視できる

簡略化された式

総GC CPUコスト ≈ (割り当て率 / (GOGC/100)) × バイト単価 × T

これは美しい!

総コスト ∝ 1/GOGC

つまり:
GOGCを2倍 → 総コストが半分

数値例で確認

前提:
割り当て率: 100 MB/秒
バイト単価: 10 ns/MB
時間T: 10秒

GOGC = 100:
総コスト = (100 / (100/100)) × 10 × 10
         = 100 × 10 × 10
         = 10,000 ns
         = 10 ms

GOGC = 200:
総コスト = (100 / (200/100)) × 10 × 10
         = 50 × 10 × 10
         = 5,000 ns
         = 5 ms
         ↑
       半分!

まとめ: 2つの関係

関係1: メモリオーバーヘッド

新規ヒープメモリ = (ライブヒープ + GCルート) × GOGC/100

∴ 新規ヒープメモリ ∝ GOGC

結論: GOGCを2倍 → メモリが2倍

関係2: CPUコスト

総GC CPUコスト ≈ (割り当て率 / (GOGC/100)) × バイト単価 × T

∴ 総GC CPUコスト ∝ 1/GOGC

結論: GOGCを2倍 → CPUコストが半分

実際の数値例

シナリオ: Webサーバー

設定:
- ライブヒープ: 100 MB
- GCルート: 20 MB
- 割り当て率: 1000 MB/秒
- バイト単価: 10 ns/MB

GOGC = 100の場合

新規ヒープ:
= (100 + 20) × 100/100
= 120 MB

GC頻度:
= 1000 / 120
≈ 8.3 回/秒

総GC CPUコスト(10秒間):
= (1000 / 1) × 10 × 10
= 100 ms

メモリ使用量:
= 100 + 120
= 220 MB

GOGC = 200の場合

新規ヒープ:
= (100 + 20) × 200/100
= 240 MB  ← 2倍

GC頻度:
= 1000 / 240
≈ 4.2 回/秒  ← 半分

総GC CPUコスト(10秒間):
= (1000 / 2) × 10 × 10
= 50 ms  ← 半分

メモリ使用量:
= 100 + 240
= 340 MB  ← 約1.5倍

視覚化

トレードオフのグラフ

GC CPUコスト
↑
│     GOGC=50
│      ●
│       \
│        \  GOGC=100
│         \  ●
│          \  \
│           \  \  GOGC=200
│            \  \  ●
│             \  \  \
└──────────────────────→ メモリ使用量

傾き = トレードオフ

重要な注意点

注意1: ヒープ構造の影響

同じサイズのライブヒープでも:

構造A: ポインタ多い
→ スキャン時間: 長い
→ CPUコスト: 高い

構造B: ポインタ少ない
→ スキャン時間: 短い
→ CPUコスト: 低い

メモリコストは同じ!

例え話:

同じ広さの部屋:

部屋A: 棚が多い
→ 掃除時間: 長い

部屋B: 棚が少ない
→ 掃除時間: 短い

面積は同じ!

注意2: 固定コストの存在

実際のGCには固定コストがある:
- データ構造の初期化
- フェーズの遷移
- など

∴ 正確には半分にはならない
でも、近似としては十分

注意3: 定常状態の前提

この式は定常状態を仮定:
- 一定の割り当て率
- 一定のライブヒープ
- 一定のバイト単価

実際のアプリは変動する
∴ 実測が重要

実用的な意味

意味1: GOGCの効果は予測可能

GOGCを調整すると:
メモリ ↔ CPU のトレードオフ

比率:
GOGC 2倍 = メモリ 2倍、CPU 半分

意味2: 最適化の方向性

GC CPUコストを減らすには:

方法1: GOGCを上げる
→ メモリ増加

方法2: 割り当て率を下げる
→ コード改善

方法3: バイト単価を下げる
→ ポインタ削減

意味3: 測定の重要性

理論:
GOGCを2倍 → CPUが半分

実際:
固定コスト、構造の影響など

∴ 必ず実測で確認

チェックリスト

GOGC調整の理解:

□ 基本式を理解した
□ 2倍-半分の関係を理解した
□ トレードオフを理解した
□ 固定コストの影響を理解した
□ 構造の影響を理解した
□ 実測の重要性を理解した

最重要ポイント:

数式は理解の助けになるが、 実際のアプリでは必ず測定!

実用的なアプローチ:

  1. 理論: 数式で効果を理解
  2. 予測: 期待される効果を計算
  3. 実測: ベンチマークで確認
  4. 調整: 結果に基づいて微調整

理論と実践の両方が大切です! 📊

おわりに 

本日は、Go言語のガベージコレクターについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました