Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 11656] ファイルの概要

このコミットは、Go言語のランタイムにおけるメモリ統計情報 (runtime.MemStats) の取得方法を根本的に変更するものです。具体的には、既存の runtime.UpdateMemStats 関数を削除し、代わりに runtime.ReadMemStats(&stats) という新しいAPIを導入しています。これにより、メモリ統計情報がグローバル変数として直接アクセスされるのではなく、ユーザーが提供する MemStats 構造体へのポインタを介して取得されるようになります。また、runtime.MemStats は非公開化され、MemStatsTypeMemStats に名称変更されています。

コミット

  • コミットハッシュ: 842c906e2e9560187d4877d9f52e8f9ceb63d84c
  • Author: Rémy Oudompheng oudomphe@phare.normalesup.org
  • Date: Mon Feb 6 19:16:26 2012 +0100

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/842c906e2e9560187d4877d9f52e8f9ceb63d84c

元コミット内容

    runtime: delete UpdateMemStats, replace with ReadMemStats(&stats).
    
    Unexports runtime.MemStats and rename MemStatsType to MemStats.
    The new accessor requires passing a pointer to a user-allocated
    MemStats structure.
    
    Fixes #2572.
    
    R=bradfitz, rsc, bradfitz, gustavo
    CC=golang-dev, remy
    https://golang.org/cl/5616072

変更の背景

この変更は、Go 1のリリース(2012年3月)に向けて、Goランタイムのメモリ統計情報 (runtime.MemStats) の扱いを改善するために行われました。以前は、runtime.MemStats はグローバル変数として公開されており、runtime.UpdateMemStats() を呼び出すことでその内容が更新されていました。しかし、このアプローチにはいくつかの課題がありました。

  1. スレッドセーフティと一貫性: グローバル変数に直接アクセスし、それを更新する形式では、複数のゴルーチンが同時にメモリ統計情報にアクセスしようとした際に、データの不整合や競合状態が発生する可能性がありました。UpdateMemStats を呼び出すことで最新の状態に更新されるものの、その更新タイミングや、更新中に他のゴルーチンが古いデータを読み取る可能性が問題となり得ました。
  2. APIの明確性: UpdateMemStats を呼び出してから MemStats を読み取るという二段階のプロセスは、APIの利用を複雑にしていました。開発者は、いつ UpdateMemStats を呼び出すべきか、そしてその呼び出しがどのような副作用をもたらすのかを常に意識する必要がありました。
  3. Go 1の安定性への貢献: Go 1は長期的な安定性と互換性を目指して設計されており、ランタイムの重要な部分であるメモリ統計情報のAPIも、より堅牢で予測可能なものにする必要がありました。Issue #2572は、このAPIの改善を具体的に提案したものです。

これらの背景から、runtime.MemStats を非公開化し、ユーザーが明示的に MemStats 構造体を割り当て、そのポインタを runtime.ReadMemStats に渡すことで統計情報を取得する、より安全で明確なAPIデザインへと変更されました。これにより、統計情報の取得がより制御され、スレッドセーフな方法で行われるようになりました。

前提知識の解説

Goのメモリ管理とガベージコレクション (GC)

Go言語は、自動メモリ管理(ガベージコレクション、GC)を採用しています。開発者はC++のように手動でメモリを解放する必要がなく、Goランタイムが不要になったメモリを自動的に回収します。GoのGCは、並行マーク&スイープ方式をベースにしており、プログラムの実行と並行して動作することで、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えるように設計されています。

runtime.MemStats 構造体

runtime.MemStats は、Goプログラムのメモリ使用状況に関する詳細な統計情報を提供する構造体です。この構造体には、以下のような多岐にわたる情報が含まれています。

  • Alloc: 現在割り当てられていて使用中のヒープメモリのバイト数。
  • TotalAlloc: プログラム開始以降に割り当てられた総バイト数(解放されたメモリも含む)。
  • Sys: オペレーティングシステムから取得したメモリの総バイト数。
  • HeapAlloc: ヒープに割り当てられているオブジェクトのバイト数。
  • HeapSys: ヒープのためにOSから取得したメモリのバイト数。
  • HeapObjects: ヒープに割り当てられているオブジェクトの数。
  • NumGC: 実行されたGCの総回数。
  • PauseTotalNs: GCによる一時停止時間の合計(ナノ秒)。
  • PauseNs: 直近のGC一時停止時間の履歴。
  • BySize: 特定のサイズクラスごとのアロケーション統計。

これらの統計情報は、Goアプリケーションのパフォーマンスチューニング、メモリリークの検出、GC動作の理解などに不可欠です。

以前のメモリ統計情報取得方法 (runtime.UpdateMemStats)

このコミット以前は、runtime.MemStats はグローバル変数として公開されており、その内容は runtime.UpdateMemStats() 関数を呼び出すことで更新されていました。開発者は、最新のメモリ統計情報を取得するために、まず runtime.UpdateMemStats() を呼び出し、その後 runtime.MemStats グローバル変数から必要なフィールドにアクセスしていました。

// 以前のコードの例
runtime.UpdateMemStats()
fmt.Printf("HeapAlloc: %d bytes\n", runtime.MemStats.HeapAlloc)

この方法は、グローバル変数への直接アクセスと、その更新を明示的にトリガーする必要があるという点で、いくつかの課題を抱えていました。

技術的詳細

このコミットによる技術的な変更は、Goランタイムのメモリ統計情報APIの設計思想を大きく変えるものです。

  1. runtime.UpdateMemStats の削除: 以前はメモリ統計情報を更新するために必要だった runtime.UpdateMemStats() 関数が完全に削除されました。これにより、開発者は明示的に統計情報の更新をトリガーする必要がなくなりました。

  2. runtime.MemStats の非公開化と名称変更:

    • 以前は runtime.MemStats という名前でグローバル変数として公開されていたものが、このコミットにより非公開化されました。
    • 同時に、src/pkg/runtime/mem.go 内で定義されていた MemStatsType という構造体の型名が MemStats に変更されました。これにより、外部からアクセス可能な MemStats 型は、新しい ReadMemStats 関数を通じてのみ利用されることになります。
    --- a/src/pkg/runtime/mem.go
    +++ b/src/pkg/runtime/mem.go
    @@ -6,9 +6,9 @@ package runtime
     
     import "unsafe"
     
    -type MemStatsType struct {
    +// A MemStats records statistics about the memory allocator.
    +type MemStats struct {
     	// General statistics.
    -	// Not locked during update; approximate.
     	Alloc      uint64 // bytes allocated and still in use
     	TotalAlloc uint64 // bytes allocated (even if freed)
     	Sys        uint64 // bytes obtained from system (should be sum of XxxSys below)
    @@ -43,7 +43,6 @@ type MemStatsType struct {
     	DebugGC      bool
     
     	// Per-size allocation statistics.
    -	// Not locked during update; approximate.
     	// 61 is NumSizeClasses in the C code.
     	BySize [61]struct {
     	\tSize    uint32
    @@ -54,21 +53,17 @@ type MemStatsType struct {\n     
     var sizeof_C_MStats uintptr // filled in by malloc.goc
     
    +var memStats MemStats
    +
     func init() {
    -\tif sizeof_C_MStats != unsafe.Sizeof(MemStats) {\n-\t\tprintln(sizeof_C_MStats, unsafe.Sizeof(MemStats))\n+\tif sizeof_C_MStats != unsafe.Sizeof(memStats) {\n+\t\tprintln(sizeof_C_MStats, unsafe.Sizeof(memStats))\n     \t\tpanic("MStats vs MemStatsType size mismatch")
     \t}
     }
     
    -// MemStats holds statistics about the memory system.
    -// The statistics may be out of date, as the information is
    -// updated lazily from per-thread caches.
    -// Use UpdateMemStats to bring the statistics up to date.
    -var MemStats MemStatsType
    -
    -// UpdateMemStats brings MemStats up to date.
    -func UpdateMemStats()
    +// ReadMemStats populates m with memory allocator statistics.
    +func ReadMemStats(m *MemStats)
     
     // GC runs a garbage collection.
     func GC()
    
  3. runtime.ReadMemStats の導入: 新しい runtime.ReadMemStats(m *MemStats) 関数が導入されました。この関数は、引数として MemStats 構造体へのポインタを受け取り、そのポインタが指す構造体に現在のメモリ統計情報を書き込みます。

    // 新しいコードの例
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("HeapAlloc: %d bytes\n", m.HeapAlloc)
    

    この変更により、メモリ統計情報の取得は以下の点で改善されました。

    • 明示的なデータ取得: 開発者は、統計情報を取得したいときに ReadMemStats を呼び出し、その結果を自身の MemStats 構造体に格納します。これにより、データのライフサイクルと所有権が明確になります。
    • スレッドセーフティの向上: ReadMemStats の内部実装(src/pkg/runtime/mgc0.c)では、統計情報を取得する際に runtime·stoptheworld()runtime·starttheworld() を使用してGCを一時停止し、一貫性のあるスナップショットを取得しています。これにより、統計情報が取得される瞬間のデータの一貫性が保証されます。
    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -997,7 +997,7 @@ runtime·gc(int32 force)\n     }\n     
     void
    -runtime·UpdateMemStats(void)\n    +runtime·ReadMemStats(MStats *stats)\n     {\n     \t// Have to acquire gcsema to stop the world,\n     \t// because stoptheworld can only be used by\n    @@ -1007,6 +1007,7 @@ runtime·UpdateMemStats(void)\n     \tm->gcing = 1;\n     \truntime·stoptheworld();\n     \tcachestats();
    +\t*stats = mstats;\n     \tm->gcing = 0;\n     \truntime·semrelease(&gcsema);\n     \truntime·starttheworld(false);
    

これらの変更は、Goのメモリ統計情報APIをより堅牢で、予測可能で、スレッドセーフなものにするための重要なステップでした。

コアとなるコードの変更箇所

このコミットによる主要なコード変更は、以下のファイルに集中しています。

  1. src/pkg/runtime/mem.go:

    • MemStatsType 構造体の名称が MemStats に変更されました。
    • runtime.MemStats グローバル変数が削除され、代わりに var memStats MemStats という非公開の変数として内部的に管理されるようになりました。
    • UpdateMemStats() 関数の宣言が削除され、ReadMemStats(m *MemStats) 関数の宣言が追加されました。
  2. src/pkg/runtime/mgc0.c:

    • C言語で実装されている runtime·UpdateMemStats 関数が runtime·ReadMemStats に名称変更され、引数として MStats *stats を受け取るように変更されました。
    • この関数内で、内部の mstats 構造体の内容が引数で渡された stats ポインタにコピーされるようになりました (*stats = mstats;)。
  3. src/cmd/godoc/godoc.go および src/pkg/*/ 以下のテストファイル群:

    • runtime.MemStats への直接アクセスや runtime.UpdateMemStats() の呼び出しが、新しい runtime.ReadMemStats() を使用する形式に一括で変更されました。これは、Goエコシステム全体にわたるAPI変更の影響を示しています。

    例: src/cmd/godoc/godoc.go

    --- a/src/cmd/godoc/godoc.go
    +++ b/src/cmd/godoc/godoc.go
    @@ -1510,9 +1510,12 @@ func updateIndex() {
     \t\tlog.Printf("index updated (%gs, %d bytes of source, %d files, %d lines, %d unique words, %d spots)",
     \t\t\tsecs, stats.Bytes, stats.Files, stats.Lines, stats.Words, stats.Spots)
     \t}
    -\tlog.Printf("before GC: bytes = %d footprint = %d", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
    +\tmemstats := new(runtime.MemStats)
    +\truntime.ReadMemStats(memstats)
    +\tlog.Printf("before GC: bytes = %d footprint = %d", memstats.HeapAlloc, memstats.Sys)
     \truntime.GC()
    -\tlog.Printf("after  GC: bytes = %d footprint = %d", runtime.MemStats.HeapAlloc, runtime.MemStats.Sys)
    +\truntime.ReadMemStats(memstats)
    +\tlog.Printf("after  GC: bytes = %d footprint = %d", memstats.HeapAlloc, memstats.Sys)
     }
     
     func indexer() {
    

コアとなるコードの解説

このコミットの核心は、メモリ統計情報の取得方法を「グローバル変数への直接アクセスと明示的な更新」から「ユーザーが提供する構造体へのポインタを介したデータコピー」へと変更した点にあります。

src/pkg/runtime/mem.go の変更

  • type MemStats struct { ... }: 以前は MemStatsType という名前だった構造体が MemStats に変更され、これが外部に公開されるメモリ統計情報の型となりました。この構造体自体は、メモリ使用量、GC回数、一時停止時間など、Goランタイムのメモリ管理に関する詳細な情報を含んでいます。

  • var memStats MemStats: この行は、ランタイム内部でメモリ統計情報を保持するための非公開の MemStats 型の変数を宣言しています。以前の var MemStats MemStatsType (公開変数) とは異なり、この memStats は外部から直接アクセスできません。

  • func ReadMemStats(m *MemStats): この関数が新しいAPIのエントリポイントです。m *MemStats という引数は、呼び出し元が MemStats 型の構造体を事前に割り当て、そのアドレス(ポインタ)をこの関数に渡す必要があることを示しています。ReadMemStats は、ランタイム内部の最新のメモリ統計情報を、この m が指す構造体にコピーします。これにより、呼び出し元は常に最新かつ一貫性のある統計情報のスナップショットを受け取ることができます。

src/pkg/runtime/mgc0.c の変更

  • void runtime·ReadMemStats(MStats *stats): C言語で実装されたこの関数は、Go言語の runtime.ReadMemStats から呼び出されます。引数の MStats *stats は、Go側から渡された MemStats 構造体へのポインタに対応します。

  • runtime·stoptheworld();runtime·starttheworld(false);: この部分が、統計情報取得の原子性を保証する重要なメカニズムです。stoptheworld() は、Goランタイム内のすべてのゴルーチン(GCゴルーチンを除く)の実行を一時停止させます。これにより、メモリ統計情報が更新される最中に他のゴルーチンがアクセスして不整合なデータを読み取ることを防ぎます。統計情報のコピーが完了した後、starttheworld() でゴルーチンの実行が再開されます。

  • *stats = mstats;: この行が、実際のデータコピーを行っています。ランタイム内部で管理されている最新のメモリ統計情報 (mstats) が、引数で渡された stats ポインタが指すメモリ領域にコピーされます。これにより、呼び出し元は、stoptheworld 中に取得された、その時点での正確なメモリ統計情報のスナップショットを受け取ることができます。

これらの変更により、Goのメモリ統計情報APIは、より安全で、より予測可能で、そしてよりGoらしい(ポインタを介した明示的なデータ受け渡し)設計へと進化しました。

関連リンク

参考にした情報源リンク