[インデックス 1378] ファイルの概要
このコミットは、Go言語のランタイムにおけるメモリ管理、特にmalloc
(メモリ確保)とスタック管理に関する重要なバグ修正と改善を目的としています。Goの初期段階におけるメモリ割り当ての堅牢性と効率性を向上させるための多岐にわたる変更が含まれています。
コミット
malloc
のバグ修正。
デフォルトでmalloc
を使用。
スタックを解放。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/da0a7d7b8f896bc2117ce488c4e245d626ef8aba
元コミット内容
malloc bug fixes.
use malloc by default.
free stacks.
R=r
DELTA=424 (333 added, 29 deleted, 62 changed)
OCL=21553
CL=21584
変更の背景
Go言語の初期開発段階において、ランタイムのメモリ管理は継続的に最適化されていました。このコミットは、特に以下の問題に対処するために導入されました。
- メモリ割り当ての堅牢性向上: 以前の
malloc
実装には、特定の条件下でバグや非効率性があった可能性があります。特に、メモリ不足時の挙動や、大規模な割り当ての処理方法に改善の余地がありました。 - スタック管理の改善: Goのゴルーチンは軽量なスタックを使用しますが、その割り当てと解放のプロセスは効率的かつ安全である必要があります。特に、
malloc
処理中にスタックが必要になった場合のデッドロックの可能性や、スタックの適切な解放が課題でした。 - デフォルトのメモリ割り当て戦略: このコミット以前は、特定の状況下で
malloc
以外のメモリ割り当てメカニズムが使用されていた可能性があります。このコミットは、より統一されたmalloc
ベースの割り当て戦略への移行を示唆しています。 - 内部データ構造の最適化: メモリキャッシュ(MCache)やセントラルフリーリスト(MCentral)などの内部データ構造が、より効率的なリンクリスト管理のために見直されました。
これらの変更は、Goプログラムの安定性、パフォーマンス、およびメモリ使用効率を全体的に向上させることを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGoランタイムおよびメモリ管理に関する基本的な知識が必要です。
- Goランタイム: Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ゴルーチン、スケジューラ、ガベージコレクタ、メモリ管理など、プログラムの実行に必要な低レベルの機能を提供します。
- メモリ割り当て (malloc): プログラムが実行時に動的にメモリを要求する際に使用される機能です。Goランタイムは独自のメモリ割り当て器を持ち、OSの
mmap
やbrk
などのシステムコールを介してメモリを確保し、それを細かく分割してアプリケーションに提供します。 - ヒープとスタック:
- ヒープ: 動的に割り当てられるメモリ領域で、プログラムの実行中にサイズが変動します。Goでは、
new
やmake
で作成されたオブジェクトや、エスケープ解析によってヒープに割り当てられる変数がここに格納されます。ガベージコレクタが管理します。 - スタック: 関数呼び出しやローカル変数が格納されるメモリ領域です。Goのゴルーチンは、必要に応じてサイズが動的に増減する「可変長スタック」を使用します。
- ヒープ: 動的に割り当てられるメモリ領域で、プログラムの実行中にサイズが変動します。Goでは、
- MCache (Per-P Cache): Goのメモリ管理において、各論理プロセッサ(P)が持つローカルなメモリキャッシュです。これにより、ロック競合を減らし、高速なメモリ割り当てを実現します。小さなオブジェクトの割り当てに特化しています。
- MCentral (Central Free List): MCacheがメモリを使い果たした際に、より大きなメモリブロックを供給する役割を担います。複数のMCache間で共有されるフリーリストを管理します。
- MHeap (Page Heap): GoランタイムがOSから取得したメモリを管理する最下層のコンポーネントです。ページ単位でメモリを管理し、MCentralに大きなメモリブロックを供給します。
- MSpan: MHeapによって管理される連続したメモリページのブロックです。MSpanは、特定のサイズクラスのオブジェクトを割り当てるために使用されます。
- FixAlloc: 固定サイズのオブジェクトを効率的に割り当てるためのシンプルなアロケータです。Goランタイム内部で、特定の内部データ構造(例: MSpanオブジェクト自体)の割り当てに使用されます。
- デッドロック: 複数のプロセスやスレッドが互いにリソースの解放を待機し、結果としてどのプロセスも進行できなくなる状態です。メモリ割り当て器のような共有リソースを扱うシステムでは、デッドロックの発生を防ぐ設計が重要です。
技術的詳細
このコミットは、Goランタイムのメモリ管理サブシステムに広範な変更を加えています。
-
malloc
のデッドロック防止と堅牢性向上:src/runtime/malloc.c
のmalloc
関数にm->mallocing
フラグが導入されました。これは、現在のM(OSスレッドを表すGoランタイムの構造体)がmalloc
処理中であることを示すフラグです。malloc
処理中に再度malloc
が呼び出された場合(例えば、スタックの拡張が必要になった場合など)、デッドロックを防ぐためにthrow("malloc - deadlock")
でパニックを発生させるようになりました。これは、再帰的なmalloc
呼び出しが予期せぬ競合状態を引き起こす可能性を排除するためです。- メモリ不足の場合の挙動が
return nil
からthrow("out of memory")
に変更されました。これにより、メモリ割り当て失敗がより明確なエラーとして扱われ、デバッグが容易になります。 - 非常に大きな割り当て(
sizeclass == 0
)の場合、MCacheを介さずに直接MHeapから割り当てるロジックが明示的に記述されました。
-
スタック管理の改善:
src/runtime/malloc.c
にstackalloc
とstackfree
関数が追加されました。これらの関数は、ゴルーチンのスタック割り当てと解放を処理します。malloc
処理中にstackalloc
が呼び出された場合、デッドロックを避けるために、FixAlloc
という固定サイズアロケータを使用するようになりました。これは、malloc
内部でのスタックフレームが小さいという仮定に基づいています。src/runtime/mem.c
からstackalloc
とstackfree
が削除され、malloc.c
に統合されました。これは、メモリ割り当てとスタック管理のロジックをより密接に連携させるための変更です。src/runtime/proc.c
とsrc/runtime/rt0_amd64.s
で、g0
(スケジューラが使用する特別なゴルーチン)の初期スタックサイズが1024バイトから8192バイトに増加されました。これは、ランタイムの安定性向上に寄与します。
-
リンクリスト管理の共通化と最適化 (
MLink
):src/runtime/malloc.h
にMLink
という新しい構造体が導入されました。これは、汎用的なリンクリストのノードとして機能し、next
ポインタを持ちます。FixAlloc
、MCacheList
、MCentral
など、ランタイム内の様々な場所で使用されていたリンクリストの実装が、このMLink
構造体を使用するように変更されました。これにより、コードの重複が減り、一貫性が向上し、将来的なメンテナンスが容易になります。MCentral_AllocList
とMCentral_FreeList
のシグネチャが変更され、void**
の代わりにMLink**
やMLink*
を使用するようになりました。MCentral_Free
において、解放されたブロックのリンクリストポインタをゼロにする処理が追加されました。これにより、ページが完全にゼロになり、デバッグやメモリの再利用が容易になります。
-
MCacheの最適化とスカベンジング:
src/runtime/mcache.c
のMCache_Free
関数に、MaxMCacheListLen
を超えた場合に中央キャッシュにチャンクを解放するロジック(ReleaseN
関数)が追加されました。MaxMCacheSize
を超えた場合に、MCache内のメモリをスカベンジング(回収)するロジックが追加されました。これは、nlistmin
という新しいフィールドを利用して、各サイズクラスで実際に必要とされた最小の要素数を追跡し、それに基づいて不要なメモリを中央キャッシュに返却するものです。これにより、MCacheのメモリフットプリントが最適化されます。
-
malloc.Lookup
の追加:src/lib/malloc.go
とsrc/runtime/malloc.c
にmalloc.Lookup
関数が追加されました。この関数は、与えられたポインタが指すメモリブロックのベースアドレスとサイズを返します。これは、デバッグやメモリプロファイリングに役立つ機能です。
-
テストの強化:
test/mallocrep1.go
という新しいテストファイルが追加されました。このテストは、様々なサイズのメモリブロックを繰り返し割り当てて解放し、malloc.Lookup
の正確性やメモリ使用量の変動を検証します。- 既存の
test/mallocrep.go
も修正され、スタック割り当てをmstats.alloc
から除外するようになり、エラーメッセージも詳細化されました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、Goランタイムのメモリ管理に関連する以下のファイルに集中しています。
src/runtime/malloc.c
:malloc
関数にm->mallocing
フラグによるデッドロック防止ロジックと、メモリ不足時のthrow
処理が追加。mlookup
関数が追加され、ポインタからメモリブロック情報を取得可能に。mal
関数がoldmal
にリネームされ、新しいmal
関数がmalloc
を呼び出すように変更。stackalloc
とstackfree
関数が追加され、malloc
処理中のスタック割り当てにFixAlloc
を使用するロジックが実装。
src/runtime/malloc.h
:MLink
構造体の定義が追加。FixAlloc
、MCacheList
、MCentral
などの構造体で、リンクリストのポインタ型がvoid*
からMLink*
に変更。MStats
にstacks
フィールドが追加され、スタック関連の統計情報が追跡可能に。M
構造体にmallocing
フィールドが追加。
src/runtime/mcache.c
:MCache_Alloc
とMCache_Free
関数がMLink
を使用するように変更。ReleaseN
関数が追加され、MCacheから中央キャッシュへのメモリ解放ロジックを実装。MCache_Free
に、MaxMCacheListLen
とMaxMCacheSize
に基づいたメモリ解放とスカベンジングのロジックが追加。
src/runtime/mcentral.c
:MCentral_AllocList
とMCentral_FreeList
関数がMLink
を使用するように変更。MCentral_Free
において、解放されたブロックのリンクリストポインタをゼロにする処理が追加。
src/runtime/mem.c
:stackalloc
とstackfree
関数が削除され、malloc.c
に移動。mal
関数がoldmal
にリネーム。
src/runtime/proc.c
:schedinit
でfindfunc(0)
が呼び出され、シンボルテーブルの初期化が早まる。matchmg
でg0
のスタックサイズが1024から8192に増加。
src/runtime/rt0_amd64.s
:_rt0_amd64
におけるスタックの初期割り当てサイズが1024から8192に増加。
src/lib/malloc.go
:export func Lookup(*byte) (*byte, uint64);
が追加され、Goコードからmalloc.Lookup
が利用可能に。
test/mallocrep1.go
:- 新しいテストファイルとして追加され、
malloc.Lookup
のテストを含む繰り返し割り当て・解放テストを実施。
- 新しいテストファイルとして追加され、
コアとなるコードの解説
src/runtime/malloc.c
の変更点
// malloc関数内の変更
if(m->mallocing)
throw("malloc - deadlock");
m->mallocing = 1;
// ... (中略) ...
if(v == nil)
throw("out of memory"); // 以前は return nil;
// ... (中略) ...
m->mallocing = 0;
return v;
// mlookup関数の追加
void
mlookup(void *v, byte **base, uintptr *size)
{
// ... (実装) ...
}
// stackalloc関数の追加
void*
stackalloc(uint32 n)
{
if(m->mallocing) {
lock(&stacks);
// ... (FixAlloc を使用した割り当て) ...
unlock(&stacks);
return v;
}
return malloc(n);
}
// stackfree関数の追加
void
stackfree(void *v)
{
if(m->mallocing) {
lock(&stacks);
FixAlloc_Free(&stacks, v);
unlock(&stacks);
return;
}
free(v);
}
malloc
関数にm->mallocing
フラグが追加されたことで、malloc
が再帰的に呼び出されることによるデッドロックを検出・防止できるようになりました。これは、例えばmalloc
が内部でスタックを拡張しようとする際に、再びmalloc
を呼び出すような状況で発生しうる問題です。return nil
がthrow("out of memory")
に変更されたことで、メモリ割り当て失敗がより明確なランタイムエラーとして扱われるようになり、デバッグが容易になります。
mlookup
関数は、任意のポインタv
がどのメモリブロックに属しているかを特定し、そのブロックの開始アドレスとサイズを返します。これは、ガベージコレクタやデバッガがメモリレイアウトを理解するために不可欠な機能です。
stackalloc
とstackfree
は、ゴルーチンのスタックを割り当て・解放するための専用関数です。特に注目すべきは、m->mallocing
が真の場合(つまり、malloc
処理中である場合)には、通常のmalloc
ではなく、FixAlloc
という固定サイズアロケータを使用する点です。これにより、malloc
内部でのスタック割り当てがデッドロックを引き起こすのを防ぎます。
src/runtime/malloc.h
の変更点
// MLink構造体の追加
struct MLink
{
MLink *next;
};
// FixAlloc構造体の変更
struct FixAlloc
{
// ...
MLink *list; // 以前は void *list;
// ...
};
// MStats構造体の変更
struct MStats
{
// ...
uint64 stacks; // 新規追加
};
// MCacheList構造体の変更
struct MCacheList
{
MLink *list; // 以前は void *list;
uint32 nlist;
uint32 nlistmin; // 新規追加
};
// MCentral_AllocList と MCentral_FreeList のシグネチャ変更
int32 MCentral_AllocList(MCentral *c, int32 n, MLink **first); // 以前は void **start, void **end
void MCentral_FreeList(MCentral *c, int32 n, MLink *first); // 以前は void *start, void *end
MLink
構造体の導入は、Goランタイム内のリンクリスト管理を標準化するための重要な変更です。これにより、FixAlloc
、MCacheList
、MCentral
など、様々な場所で使われるフリーリストの管理が統一され、コードの可読性と保守性が向上します。
MStats
にstacks
フィールドが追加されたことで、ランタイムがスタックに割り当てたメモリ量を追跡できるようになり、メモリ使用量の統計がより詳細になりました。
MCacheList
にnlistmin
が追加されたのは、MCacheのスカベンジングロジックをサポートするためです。これは、MCacheが中央キャッシュから取得したメモリのうち、実際に使用された最小量を記録し、それに基づいて不要なメモリを中央キャッシュに返却する判断材料となります。
MCentral_AllocList
とMCentral_FreeList
のシグネチャがMLink
を使用するように変更されたのは、リンクリストの統一化の一環です。
src/runtime/mcache.c
の変更点
// MCache_Alloc関数内の変更
MLink *first, *v; // 以前は void *v, *start, *end;
// ... (中略) ...
v = l->list;
l->list = v->next; // 以前は *(void**)v;
l->nlist--;
if(l->nlist < l->nlistmin)
l->nlistmin = l->nlist; // nlistmin の更新
// ...
v->next = nil; // 以前は *(void**)v = nil;
// ReleaseN関数の追加
static void
ReleaseN(MCache *c, MCacheList *l, int32 n, int32 sizeclass)
{
// ... (実装) ...
}
// MCache_Free関数内の変更
MLink *p; // 新規追加
// ... (中略) ...
p = v;
p->next = l->list; // 以前は *(void**)p = l->list;
l->list = p;
l->nlist++;
// ...
if(l->nlist >= MaxMCacheListLen) {
ReleaseN(c, l, class_to_transfercount[sizeclass], sizeclass); // 中央キャッシュへの解放
}
if(c->size >= MaxMCacheSize) {
// Scavenge.
for(i=0; i<NumSizeClasses; i++) {
l = &c->list[i];
n = l->nlistmin;
if(n > 0) {
if(n > 1)
n /= 2;
ReleaseN(c, l, n, i);
}
l->nlistmin = l->nlist; // nlistmin のリセット
}
}
MCache_Alloc
とMCache_Free
は、MLink
構造体を使用するように変更され、リンクリストの操作がより型安全になりました。
MCache_Free
におけるReleaseN
の呼び出しは、MCacheが一定量以上のオブジェクトを保持している場合に、それらの一部を中央キャッシュに返却するロジックです。これにより、MCacheが過剰にメモリを保持するのを防ぎます。
さらに、MCache_Free
にはスカベンジングロジックが追加されました。これは、MCache全体のサイズがMaxMCacheSize
を超えた場合に実行されます。各サイズクラスのnlistmin
(最後にスカベンジングされてからリストに存在した最小の要素数)をチェックし、その値に基づいて不要なメモリを中央キャッシュに返却します。これにより、MCacheのメモリフットプリントが動的に調整され、メモリ効率が向上します。
src/runtime/mcentral.c
の変更点
// MCentral_AllocList関数の変更
int32
MCentral_AllocList(MCentral *c, int32 n, MLink **pfirst) // 以前は void **pstart, void **pend
{
// ... (中略) ...
*pfirst = first;
return i;
}
// MCentral_FreeList関数の変更
void
MCentral_FreeList(MCentral *c, int32 n, void *start) // 以前は void *start, void *end
{
MLink *v, *next; // 新規追加
// ... (中略) ...
for(v=start; v; v=next) {
next = v->next; // 以前は *(void**)v;
MCentral_Free(c, v);
}
// ...
}
// MCentral_Free関数内の変更
MLink *p, *next; // 新規追加
// ... (中略) ...
p = v;
p->next = s->freelist; // 以前は *(void**)v = s->freelist;
s->freelist = p;
// ...
if(--s->ref == 0) {
// Freed blocks are zeroed except for the link pointer.
// Zero the link pointers so that the page is all zero.
for(p=s->freelist; p; p=next) {
next = p->next;
p->next = nil; // リンクリストポインタをゼロにする
}
s->freelist = nil;
// ...
}
MCentral_AllocList
とMCentral_FreeList
もMLink
を使用するように変更され、リンクリストの操作が統一されました。
MCentral_Free
において、MSpanが完全に解放される際に、そのMSpan内のフリーリストのリンクリストポインタをnil
に設定する処理が追加されました。これにより、解放されたメモリページが完全にゼロで埋められるようになり、セキュリティとデバッグの観点から望ましい状態になります。
関連リンク
- Go言語のメモリ管理に関する公式ドキュメントやブログ記事 (当時のものがあれば)
- Goランタイムのソースコードリポジトリ
参考にした情報源リンク
- Go言語のソースコード (特に
src/runtime
ディレクトリ) - Go言語のコミット履歴
- Go言語の初期の設計ドキュメント (もし公開されていれば)
- 一般的なメモリ管理アルゴリズムに関する資料 (フリーリスト、ページングなど)
- ガベージコレクションの基本原理に関する資料
- GoのIssueトラッカーやメーリングリスト (当時の議論があれば)