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

[インデックス 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言語の初期開発段階において、ランタイムのメモリ管理は継続的に最適化されていました。このコミットは、特に以下の問題に対処するために導入されました。

  1. メモリ割り当ての堅牢性向上: 以前のmalloc実装には、特定の条件下でバグや非効率性があった可能性があります。特に、メモリ不足時の挙動や、大規模な割り当ての処理方法に改善の余地がありました。
  2. スタック管理の改善: Goのゴルーチンは軽量なスタックを使用しますが、その割り当てと解放のプロセスは効率的かつ安全である必要があります。特に、malloc処理中にスタックが必要になった場合のデッドロックの可能性や、スタックの適切な解放が課題でした。
  3. デフォルトのメモリ割り当て戦略: このコミット以前は、特定の状況下でmalloc以外のメモリ割り当てメカニズムが使用されていた可能性があります。このコミットは、より統一されたmallocベースの割り当て戦略への移行を示唆しています。
  4. 内部データ構造の最適化: メモリキャッシュ(MCache)やセントラルフリーリスト(MCentral)などの内部データ構造が、より効率的なリンクリスト管理のために見直されました。

これらの変更は、Goプログラムの安定性、パフォーマンス、およびメモリ使用効率を全体的に向上させることを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGoランタイムおよびメモリ管理に関する基本的な知識が必要です。

  • Goランタイム: Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ゴルーチン、スケジューラ、ガベージコレクタ、メモリ管理など、プログラムの実行に必要な低レベルの機能を提供します。
  • メモリ割り当て (malloc): プログラムが実行時に動的にメモリを要求する際に使用される機能です。Goランタイムは独自のメモリ割り当て器を持ち、OSのmmapbrkなどのシステムコールを介してメモリを確保し、それを細かく分割してアプリケーションに提供します。
  • ヒープとスタック:
    • ヒープ: 動的に割り当てられるメモリ領域で、プログラムの実行中にサイズが変動します。Goでは、newmakeで作成されたオブジェクトや、エスケープ解析によってヒープに割り当てられる変数がここに格納されます。ガベージコレクタが管理します。
    • スタック: 関数呼び出しやローカル変数が格納されるメモリ領域です。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ランタイムのメモリ管理サブシステムに広範な変更を加えています。

  1. mallocのデッドロック防止と堅牢性向上:

    • src/runtime/malloc.cmalloc関数にm->mallocingフラグが導入されました。これは、現在のM(OSスレッドを表すGoランタイムの構造体)がmalloc処理中であることを示すフラグです。
    • malloc処理中に再度mallocが呼び出された場合(例えば、スタックの拡張が必要になった場合など)、デッドロックを防ぐためにthrow("malloc - deadlock")でパニックを発生させるようになりました。これは、再帰的なmalloc呼び出しが予期せぬ競合状態を引き起こす可能性を排除するためです。
    • メモリ不足の場合の挙動がreturn nilからthrow("out of memory")に変更されました。これにより、メモリ割り当て失敗がより明確なエラーとして扱われ、デバッグが容易になります。
    • 非常に大きな割り当て(sizeclass == 0)の場合、MCacheを介さずに直接MHeapから割り当てるロジックが明示的に記述されました。
  2. スタック管理の改善:

    • src/runtime/malloc.cstackallocstackfree関数が追加されました。これらの関数は、ゴルーチンのスタック割り当てと解放を処理します。
    • malloc処理中にstackallocが呼び出された場合、デッドロックを避けるために、FixAllocという固定サイズアロケータを使用するようになりました。これは、malloc内部でのスタックフレームが小さいという仮定に基づいています。
    • src/runtime/mem.cからstackallocstackfreeが削除され、malloc.cに統合されました。これは、メモリ割り当てとスタック管理のロジックをより密接に連携させるための変更です。
    • src/runtime/proc.csrc/runtime/rt0_amd64.sで、g0(スケジューラが使用する特別なゴルーチン)の初期スタックサイズが1024バイトから8192バイトに増加されました。これは、ランタイムの安定性向上に寄与します。
  3. リンクリスト管理の共通化と最適化 (MLink):

    • src/runtime/malloc.hMLinkという新しい構造体が導入されました。これは、汎用的なリンクリストのノードとして機能し、nextポインタを持ちます。
    • FixAllocMCacheListMCentralなど、ランタイム内の様々な場所で使用されていたリンクリストの実装が、このMLink構造体を使用するように変更されました。これにより、コードの重複が減り、一貫性が向上し、将来的なメンテナンスが容易になります。
    • MCentral_AllocListMCentral_FreeListのシグネチャが変更され、void**の代わりにMLink**MLink*を使用するようになりました。
    • MCentral_Freeにおいて、解放されたブロックのリンクリストポインタをゼロにする処理が追加されました。これにより、ページが完全にゼロになり、デバッグやメモリの再利用が容易になります。
  4. MCacheの最適化とスカベンジング:

    • src/runtime/mcache.cMCache_Free関数に、MaxMCacheListLenを超えた場合に中央キャッシュにチャンクを解放するロジック(ReleaseN関数)が追加されました。
    • MaxMCacheSizeを超えた場合に、MCache内のメモリをスカベンジング(回収)するロジックが追加されました。これは、nlistminという新しいフィールドを利用して、各サイズクラスで実際に必要とされた最小の要素数を追跡し、それに基づいて不要なメモリを中央キャッシュに返却するものです。これにより、MCacheのメモリフットプリントが最適化されます。
  5. malloc.Lookupの追加:

    • src/lib/malloc.gosrc/runtime/malloc.cmalloc.Lookup関数が追加されました。この関数は、与えられたポインタが指すメモリブロックのベースアドレスとサイズを返します。これは、デバッグやメモリプロファイリングに役立つ機能です。
  6. テストの強化:

    • test/mallocrep1.goという新しいテストファイルが追加されました。このテストは、様々なサイズのメモリブロックを繰り返し割り当てて解放し、malloc.Lookupの正確性やメモリ使用量の変動を検証します。
    • 既存のtest/mallocrep.goも修正され、スタック割り当てをmstats.allocから除外するようになり、エラーメッセージも詳細化されました。

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

このコミットにおける主要なコード変更は、Goランタイムのメモリ管理に関連する以下のファイルに集中しています。

  • src/runtime/malloc.c:
    • malloc関数にm->mallocingフラグによるデッドロック防止ロジックと、メモリ不足時のthrow処理が追加。
    • mlookup関数が追加され、ポインタからメモリブロック情報を取得可能に。
    • mal関数がoldmalにリネームされ、新しいmal関数がmallocを呼び出すように変更。
    • stackallocstackfree関数が追加され、malloc処理中のスタック割り当てにFixAllocを使用するロジックが実装。
  • src/runtime/malloc.h:
    • MLink構造体の定義が追加。
    • FixAllocMCacheListMCentralなどの構造体で、リンクリストのポインタ型がvoid*からMLink*に変更。
    • MStatsstacksフィールドが追加され、スタック関連の統計情報が追跡可能に。
    • M構造体にmallocingフィールドが追加。
  • src/runtime/mcache.c:
    • MCache_AllocMCache_Free関数がMLinkを使用するように変更。
    • ReleaseN関数が追加され、MCacheから中央キャッシュへのメモリ解放ロジックを実装。
    • MCache_Freeに、MaxMCacheListLenMaxMCacheSizeに基づいたメモリ解放とスカベンジングのロジックが追加。
  • src/runtime/mcentral.c:
    • MCentral_AllocListMCentral_FreeList関数がMLinkを使用するように変更。
    • MCentral_Freeにおいて、解放されたブロックのリンクリストポインタをゼロにする処理が追加。
  • src/runtime/mem.c:
    • stackallocstackfree関数が削除され、malloc.cに移動。
    • mal関数がoldmalにリネーム。
  • src/runtime/proc.c:
    • schedinitfindfunc(0)が呼び出され、シンボルテーブルの初期化が早まる。
    • matchmgg0のスタックサイズが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 nilthrow("out of memory")に変更されたことで、メモリ割り当て失敗がより明確なランタイムエラーとして扱われるようになり、デバッグが容易になります。

mlookup関数は、任意のポインタvがどのメモリブロックに属しているかを特定し、そのブロックの開始アドレスとサイズを返します。これは、ガベージコレクタやデバッガがメモリレイアウトを理解するために不可欠な機能です。

stackallocstackfreeは、ゴルーチンのスタックを割り当て・解放するための専用関数です。特に注目すべきは、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ランタイム内のリンクリスト管理を標準化するための重要な変更です。これにより、FixAllocMCacheListMCentralなど、様々な場所で使われるフリーリストの管理が統一され、コードの可読性と保守性が向上します。

MStatsstacksフィールドが追加されたことで、ランタイムがスタックに割り当てたメモリ量を追跡できるようになり、メモリ使用量の統計がより詳細になりました。

MCacheListnlistminが追加されたのは、MCacheのスカベンジングロジックをサポートするためです。これは、MCacheが中央キャッシュから取得したメモリのうち、実際に使用された最小量を記録し、それに基づいて不要なメモリを中央キャッシュに返却する判断材料となります。

MCentral_AllocListMCentral_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_AllocMCache_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_AllocListMCentral_FreeListMLinkを使用するように変更され、リンクリストの操作が統一されました。

MCentral_Freeにおいて、MSpanが完全に解放される際に、そのMSpan内のフリーリストのリンクリストポインタをnilに設定する処理が追加されました。これにより、解放されたメモリページが完全にゼロで埋められるようになり、セキュリティとデバッグの観点から望ましい状態になります。

関連リンク

  • Go言語のメモリ管理に関する公式ドキュメントやブログ記事 (当時のものがあれば)
  • Goランタイムのソースコードリポジトリ

参考にした情報源リンク

  • Go言語のソースコード (特にsrc/runtimeディレクトリ)
  • Go言語のコミット履歴
  • Go言語の初期の設計ドキュメント (もし公開されていれば)
  • 一般的なメモリ管理アルゴリズムに関する資料 (フリーリスト、ページングなど)
  • ガベージコレクションの基本原理に関する資料
  • GoのIssueトラッカーやメーリングリスト (当時の議論があれば)