[インデックス 16517] ファイルの概要
このコミットは、Goランタイムのメモリプロファイラ (mprof
) におけるメモリ割り当てメカニズムの変更に関するものです。具体的には、プロファイラ内で使用されていたカスタムのメモリ割り当て関数を廃止し、Goランタイムが提供する persistentalloc
関数に置き換えることで、コードの重複を排除し、より一貫性のあるメモリ管理を実現しています。
コミット
commit 8cf7044983077a5d739d54c8deeb952a4b6b152c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Sun Jun 9 21:38:37 2013 +0400
runtime: use persistentalloc instead of custom malloc in memory profiler
Removes code duplication.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/9874055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8cf7044983077a5d739d54c8deeb952a4b6b152c
元コミット内容
runtime: use persistentalloc instead of custom malloc in memory profiler
Removes code duplication.
変更の背景
Goのメモリプロファイラは、プログラムのメモリ使用状況を追跡し、どのコードパスがメモリを割り当てているかを特定するための重要なツールです。このプロファイラ自体もメモリを割り当てる必要がありますが、その割り当てがプロファイラの測定結果に影響を与えたり、ガベージコレクションの対象になったりすることは望ましくありません。プロファイラが使用するメモリは、ガベージコレクタのオーバーヘッドを避けるため、またプロファイリング対象のメモリと混同されないように、特別な方法で管理される必要があります。
このコミット以前は、メモリプロファイラは独自のカスタムメモリ割り当てメカニズム(allocate
関数)を持っていました。これは、プロファイラが使用するメモリがガベージコレクタの管理下に入らないようにするため、およびプロファイラ自身のオーバーヘッドを最小限に抑えるために設計されたものと考えられます。しかし、Goランタイムには既に、ガベージコレクションの対象とならない永続的なメモリを割り当てるための persistentalloc
という内部関数が存在していました。
この状況は、機能的に重複するコードが存在することを意味し、メンテナンスの複雑さや潜在的なバグのリスクを増大させていました。このコミットの目的は、メモリプロファイラ内のカスタム割り当てロジックを、既存のより汎用的な persistentalloc
に置き換えることで、コードの重複を排除し、ランタイム全体のメモリ管理の一貫性を向上させることにありました。これにより、ランタイムのコードベースがよりクリーンで保守しやすくなります。
前提知識の解説
Goランタイムとメモリ管理
Goプログラムは、Goランタイムによって管理されるメモリ上で動作します。Goランタイムは、ヒープメモリの割り当て、ガベージコレクション(GC)、スタック管理など、低レベルのメモリ操作を処理します。Goのメモリ管理は、開発者が手動でメモリを解放する手間を省き、メモリリークのリスクを低減することを目的としています。
ガベージコレクション (GC)
Goは自動メモリ管理(ガベージコレクション)を採用しています。これは、プログラムが不要になったメモリを自動的に解放する仕組みです。GoのGCは、並行マーク&スイープ方式をベースにしており、プログラムの実行と並行して動作することで、アプリケーションの一時停止(ストップ・ザ・ワールド)時間を最小限に抑えるように設計されています。しかし、ランタイム内部の特定のデータ構造(例えば、GC自体が使用するデータや、プロファイラが使用するデータ)は、GCの対象とならない「永続的な」メモリ領域に配置される必要があります。これは、GCがそれらのデータ構造を誤って解放してしまわないようにするため、あるいはGCのサイクル中にそれらのデータ構造が常に利用可能である必要があるためです。
メモリプロファイラ (mprof)
Goのメモリプロファイラは、runtime/pprof
パッケージを通じて利用できるツールの一部です。これは、アプリケーションが実行中にどこで、どれくらいのメモリを割り当てているかを詳細に記録します。プロファイラは、メモリリークの特定やメモリ使用量の最適化に役立ちます。プロファイラ自体がメモリを割り当てる際、その割り当てがプロファイリングの対象とならないように、またGCのオーバーヘッドを避けるために特別な考慮が必要です。プロファイラが自身のデータをGC対象のヒープに割り当ててしまうと、プロファイリング結果が不正確になったり、プロファイラ自身の動作がGCによって中断されたりする可能性があります。
persistentalloc
とは
persistentalloc
は、Goランタイム内部で使用される低レベルのメモリ割り当て関数です。この関数によって割り当てられたメモリは、Goのガベージコレクタの管理下には置かれません。これは主に、ランタイムの内部データ構造(例えば、スケジューラ、GC、プロファイラなどが使用するメタデータ)や、GCの動作に影響を与えてはならないクリティカルなデータのために使用されます。persistentalloc
は、OSから直接メモリを要求するか、ランタイムが管理する特別な永続メモリプールからメモリを割り当てます。このメモリは、プログラムの実行期間中、明示的に解放されるまで(またはプログラム終了まで)保持されます。この関数は、Goのソースコード内で runtime/malloc.go
や runtime/mheap.go
といったファイルで定義されており、ランタイムの初期化フェーズや、GCに依存しないデータ構造の構築に利用されます。
技術的詳細
このコミットの核心は、src/pkg/runtime/mprof.goc
ファイル内のメモリ割り当てロジックの変更です。
変更前は、mprof.goc
内に allocate
という静的関数が定義されており、これがメモリプロファイラが必要とするメモリ(Bucket
構造体やハッシュテーブルのエントリなど)を割り当てていました。この allocate
関数は、pool
と poolfree
という変数を使って独自のメモリプールを管理し、必要に応じて runtime·SysAlloc
を呼び出してOSから新しいチャンク(Chunk = 32*PageSize
)を取得していました。また、alloclock
というロックを使用して、このカスタムメモリプールのアクセスを同期していました。このカスタム実装は、プロファイラがGCの影響を受けないメモリを使用するという目的は達成していましたが、Goランタイムの他の部分で既に同様の目的のために persistentalloc
が存在していたため、コードの重複と冗長性がありました。
このコミットでは、以下の変更が行われました。
- カスタム割り当てロジックの削除:
pool
,poolfree
,Chunk
といったカスタムメモリプール管理のための変数、およびalloclock
が削除されました。最も重要な変更は、allocate
静的関数が完全に削除されたことです。これにより、メモリプロファイラは独自のメモリ管理ロジックを持つ必要がなくなりました。 persistentalloc
への置き換え:mprof.goc
内でallocate
が呼び出されていたすべての箇所が、runtime·persistentalloc
の呼び出しに置き換えられました。これにより、メモリプロファイラはランタイムの標準的な永続メモリ割り当てメカニズムを利用するようになりました。runtime·persistentalloc
は、割り当てサイズとアライメント(このコミットでは0
が指定されており、これはデフォルトのアライメントを意味します)を引数として取ります。- コードの簡素化: カスタム割り当てロジックが削除されたことで、
mprof.goc
ファイルのコード行数が大幅に削減され(45行中40行削除、5行追加)、全体的なコードベースが簡素化されました。これにより、コードの可読性と保守性が向上しました。
この変更により、メモリプロファイラは、ランタイムの他の部分と同様に、GCの対象とならないメモリを割り当てるための統一されたメカニズムを使用するようになります。これは、コードの保守性を高め、将来的な変更や最適化を容易にする効果があります。また、persistentalloc
はランタイム全体で最適化されている可能性があり、プロファイラのパフォーマンスにも良い影響を与える可能性があります。
コアとなるコードの変更箇所
src/pkg/runtime/mprof.goc
ファイルにおいて、以下の変更が行われました。
-
alloclock
変数の削除:-static Lock proflock, alloclock; +static Lock proflock;
alloclock
はカスタムのallocate
関数で使用されていたロックであり、その関数が削除されたため不要になりました。 -
カスタムメモリプール関連の変数と定数の削除:
-static byte *pool; // memory allocation pool -static uintptr poolfree; // number of bytes left in the pool -enum { - Chunk = 32*PageSize, // initial size of the pool -};
これらはカスタムのメモリプールを管理するための変数と定数であり、カスタム割り当てロジックの削除に伴い不要になりました。
-
allocate
関数の削除:-// Memory allocation local to this file. -// There is no way to return the allocated memory back to the OS. -static void* -allocate(uintptr size) -{ - void *v; - - if(size == 0) - return nil; - - if(size >= Chunk/2) - return runtime·SysAlloc(size); - - runtime·lock(&alloclock); - if(size > poolfree) { - pool = runtime·SysAlloc(Chunk); - if(pool == nil) - runtime·throw("runtime: cannot allocate memory"); - poolfree = Chunk; - } - v = pool; - pool += size; - poolfree -= size; - runtime·unlock(&alloclock); - return v; -}
この関数は、メモリプロファイラが使用するメモリを独自に管理するためのものでしたが、
persistentalloc
への移行によりその役割を終えました。 -
allocate
の呼び出しをruntime·persistentalloc
に置き換え:-
stkbucket
関数内:- b = allocate(sizeof *b + nstk*sizeof stk[0]); - if(b == nil) - runtime·throw("runtime: cannot allocate memory"); + b = runtime·persistentalloc(sizeof *b + nstk*sizeof stk[0], 0);
stkbucket
はコールスタックの情報を格納するBucket
構造体を割り当てていました。 -
setaddrbucket
関数内(2箇所):- ah = allocate(sizeof *ah); + ah = runtime·persistentalloc(sizeof *ah, 0);
- e = allocate(64*sizeof *e); + e = runtime·persistentalloc(64*sizeof *e, 0);
setaddrbucket
はアドレスハッシュテーブルのエントリを割り当てていました。 -
runtime·mprofinit
関数内:- addrhash = allocate((1<<AddrHashBits)*sizeof *addrhash); + addrhash = runtime·persistentalloc((1<<AddrHashBits)*sizeof *addrhash, 0);
runtime·mprofinit
はメモリプロファイラの初期化時にアドレスハッシュテーブル自体を割り当てていました。
-
これらの変更により、メモリプロファイラのメモリ割り当ては、Goランタイムの標準的な永続メモリ割り当てメカニズムに完全に統合されました。
コアとなるコードの解説
このコミットの主要な変更は、メモリプロファイラが内部データ構造(Bucket
、AddrHash
、AddrEntry
など)を割り当てる際に使用していたカスタムの allocate
関数を、Goランタイムが提供する runtime·persistentalloc
関数に置き換えた点です。
-
allocate
関数の役割と問題点: 変更前のallocate
関数は、mprof.goc
内で定義されたプライベートなメモリ割り当て関数でした。これは、プロファイラが使用するメモリがGoのガベージコレクタの対象とならないようにするために、独自のメモリプール(pool
とpoolfree
)を管理していました。このカスタム実装は、runtime·SysAlloc
を直接呼び出してOSからメモリを取得し、そのメモリをプロファイラ専用のプールとして管理していました。また、複数のゴルーチンからのアクセスを保護するためにalloclock
を使用していました。しかし、このようなカスタム実装は、ランタイム全体でメモリ管理のロジックが分散し、コードの重複やメンテナンスの複雑さを招いていました。特に、ランタイムの他の部分で既に同様の目的のためにpersistentalloc
が存在していたため、このカスタム実装は冗長でした。 -
runtime·persistentalloc
への移行:runtime·persistentalloc
は、Goランタイムが内部的に使用する、ガベージコレクションの対象とならないメモリを割り当てるための標準的な関数です。この関数は、ランタイムの起動時や実行中に、GCのメタデータ、スケジューラのデータ構造、プロファイラのデータなど、永続的に存在する必要があるがGCの対象にはしたくないメモリ領域を確保するために設計されています。runtime·persistentalloc
を使用することで、メモリプロファイラは独自のメモリ管理ロジックを持つ必要がなくなり、ランタイム全体のメモリ管理フレームワークに統合されます。これにより、コードの重複が解消され、mprof.goc
ファイルが大幅に簡素化されました。この関数は、割り当てサイズとアライメント(このコミットでは0
が指定されており、これはデフォルトのアライメント、通常はポインタサイズに揃えることを意味します)を引数として取ります。デフォルトのアライメントは、CPUが効率的にメモリにアクセスするために重要です。
この変更は、Goランタイムの内部構造をよりクリーンで一貫性のあるものにし、将来的な開発や最適化の基盤を強化するものです。メモリプロファイラが使用するメモリがGCの対象外であるという特性は維持しつつ、その実装がより標準化された方法で行われるようになりました。これにより、ランタイム全体の整合性が高まり、デバッグや機能拡張が容易になります。
関連リンク
- Goのメモリプロファイリングに関する公式ドキュメント: https://go.dev/blog/pprof
- Goのガベージコレクションに関する情報: https://go.dev/doc/gc-guide
- Goのソースコードリポジトリ: https://github.com/golang/go
参考にした情報源リンク
- Goのソースコード (特に
src/runtime/malloc.go
,src/runtime/mprof.go
,src/runtime/mheap.go
の関連部分) - GoのIssueトラッカーや変更履歴 (Go CLs)
- Goに関する技術ブログや解説記事 (特に
persistentalloc
や Goのメモリ管理に関するもの) - Goのガベージコレクションに関する技術文書