[インデックス 16424] ファイルの概要
このコミットは、Goランタイムにおけるメモリ管理の最適化に関するものです。具体的には、起動時に行われていた256MBのページテーブルの事前割り当てを廃止し、必要に応じて(遅延的に)割り当てるように変更することで、起動時のメモリ消費量を削減し、ulimit
設定との競合を解消することを目的としています。また、この変更はGC(ガベージコレクション)における不要なメモリアクセスを排除する可能性も秘めています。
コミット
- コミットハッシュ:
671814b9044bebd9f5801cf83df74acbdf31d732
- 作者: Dmitriy Vyukov dvyukov@google.com
- コミット日時: 2013年5月28日 火曜日 22:04:34 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/671814b9044bebd9f5801cf83df74acbdf31d732
元コミット内容
runtime: allocate page table lazily
This removes the 256MB memory allocation at startup,
which conflicts with ulimit.
Also will allow to eliminate an unnecessary memory dereference in GC,
because the page table is usually mapped at known address.
Update #5049.
Update #5236.
R=golang-dev, khr, r, khr, rsc
CC=golang-dev
https://golang.org/cl/9791044
変更の背景
この変更の主な背景には、Goランタイムが起動時に行っていた256MBという大きなメモリ領域の事前割り当てが、特定の環境下で問題を引き起こしていたことがあります。
-
ulimit
との競合:ulimit
はUnix系OSにおけるプロセスが利用できるリソース(メモリ、ファイルディスクリプタなど)を制限する機能です。特にRLIMIT_DATA
のようなデータセグメントのサイズを制限する設定がされている場合、Goランタイムが起動時に256MBものメモリを確保しようとすると、このulimit
制限に抵触し、プログラムが起動できない、あるいは異常終了する問題が発生していました。Goのメモリ管理はmmap()
を使用してOSから大きなメモリ領域を要求するため、従来のbrk()
で管理されるデータセグメントのulimit
とは直接関係しないことが多いですが、RLIMIT_AS
(アドレス空間の制限)のようなより広範な制限が適用される場合には影響を受けます。このコミットは、この起動時の大きな割り当てを遅延させることで、ulimit
による起動失敗のリスクを軽減します。 -
GCの最適化の可能性: コミットメッセージには「GCにおける不要なメモリアクセスを排除する可能性」も言及されています。ページテーブルが既知のアドレスにマッピングされることで、GCがメモリを走査する際に、ページテーブルの情報をより効率的に利用できるようになることが示唆されています。これにより、GCのパフォーマンス向上やレイテンシの削減に寄与する可能性があります。
-
関連するIssue:
Update #5049
: Web検索では直接的な「GC issue 5049」は見つかりませんでしたが、Goのメモリ管理やGCに関する一般的な課題、特にメモリリークと誤解されがちな挙動(ゴルーチンのハング、グローバルキャッシュの肥大化など)が議論されることがあります。このコミットは、起動時のメモリフットプリントを減らすことで、全体的なメモリ管理の健全性に貢献します。Update #5236
: Web検索ではGoに関連する「issue 5236」は見つかりませんでした。これは内部的なIssueトラッカーの番号であるか、あるいは公開されていない情報である可能性があります。しかし、文脈から判断すると、これも起動時のメモリ割り当てやulimit
に関連する問題であったと推測されます。
これらの問題に対処するため、Goランタイムのメモリ管理メカニズム、特にページテーブルの割り当て戦略が見直され、遅延割り当てが導入されました。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Goランタイムのメモリ管理:
- Goは独自のメモリマネージャを持ち、OSから大きな仮想メモリ領域を
mmap()
システムコール(Unix系)やVirtualAlloc
(Windows)などを用いて取得します。これは従来のC/C++プログラムがbrk()
システムコールでデータセグメントを拡張するのとは異なります。 - Goのヒープは、これらのOSから取得した仮想メモリ領域(アリーナ)内に構築されます。
- Goランタイムは、ヒープ内のメモリブロックを管理するために、内部的に様々なデータ構造を使用します。これには、アリーナ内の各ページがどのように使用されているかを追跡する「ページテーブル」や、メモリブロックの割り当て状態を管理する「ビットマップ」などが含まれます。
- Goは独自のメモリマネージャを持ち、OSから大きな仮想メモリ領域を
-
ページテーブル (Page Table):
- オペレーティングシステム(OS)の仮想記憶管理において、仮想アドレスを物理アドレスに変換するために使用されるデータ構造です。各プロセスは独自のページテーブルを持ちます。
- Goランタイムの文脈では、Goが管理するヒープ領域内の仮想アドレス空間を、より効率的に管理するための内部的なマッピング構造を指すことがあります。このコミットで言及されている「ページテーブル」は、Goランタイムがヒープ内のメモリブロック(スパン)を管理するために使用する
MSpan
構造体へのポインタの配列(MHeap.map
)を指しています。これは、特定のアドレスがどのMSpan
に属するかを高速にルックアップするために使われます。
-
ulimit
:- Unix系OSのシェルコマンドで、ユーザーが起動するプロセスが利用できるシステムリソース(CPU時間、ファイルサイズ、メモリなど)に制限を設定するために使用されます。
RLIMIT_DATA
: プロセスのデータセグメントの最大サイズを制限します。RLIMIT_AS
(Address Space Limit): プロセスの仮想アドレス空間の最大サイズを制限します。Goのようにmmap()
を多用するアプリケーションでは、RLIMIT_DATA
よりもRLIMIT_AS
が影響を与える可能性が高いです。
-
SysAlloc
,SysReserve
,SysMap
:- これらはGoランタイムがOSからメモリを要求する際に使用する低レベルの関数です。
SysReserve
: 仮想アドレス空間を予約しますが、物理メモリは割り当てません。これは、将来的に使用するアドレス範囲を確保するもので、コミットメッセージの「256MB memory allocation at startup」が指すのは、この予約プロセスの一部であったと考えられます。SysMap
: 予約された仮想アドレス空間に物理メモリをマッピングします。実際にメモリが使用可能になるのはこの段階です。SysAlloc
:SysReserve
とSysMap
を組み合わせて、仮想アドレス空間の予約と物理メモリのマッピングを同時に行います。
-
Goのガベージコレクション (GC):
- GoのGCは、不要になったメモリを自動的に解放する仕組みです。Go 1.5以降は並行(concurrent)かつ非移動(non-moving)のマーク&スイープ方式を採用しており、STW(Stop-The-World)時間を最小限に抑えるように設計されています。
- GCはヒープ全体を走査し、到達可能なオブジェクトをマークし、マークされなかったオブジェクトを解放します。このプロセスにおいて、メモリのレイアウトや内部データ構造(ページテーブルなど)へのアクセス効率がGCのパフォーマンスに影響を与えます。
技術的詳細
このコミットの核心は、Goランタイムのヒープ管理構造であるMHeap
のmap
フィールド(ページテーブルに相当)の割り当て方法を、起動時の事前割り当てから遅延割り当てへと変更した点にあります。
以前のGoランタイムでは、MHeap.map
は起動時に固定サイズ(例えば256MB)の仮想アドレス空間をSysReserve
で確保していました。これは、将来的にヒープが拡張された際に、map
がヒープ全体をカバーできるようにするためでした。しかし、この事前割り当ては、たとえ実際にその領域が使用されなくても、仮想アドレス空間を消費し、特にulimit
が厳しく設定されている環境では問題となっていました。
この変更により、MHeap.map
は起動時には最小限の領域しか確保せず、ヒープが実際に拡張され、より大きなmap
領域が必要になった場合にのみ、SysMap
を呼び出して物理メモリをマッピングし、map
のサイズを動的に拡張するようになりました。
具体的には、以下の点が変更されました。
MHeap.map
の型変更:MSpan *map[1<<MHeapMap_Bits]
という固定サイズの配列から、MSpan** map
というポインタ型に変更されました。これにより、map
自体が動的に割り当てられるようになりました。spans_size
の導入:arena_size
(ヒープ領域のサイズ)とbitmap_size
(ビットマップのサイズ)に加えて、spans_size
という変数が導入されました。これはMHeap.map
が占めるべきサイズを計算するために使用されます。SysReserve
の引数変更:runtime·SysReserve
の呼び出しにおいて、予約するメモリサイズにspans_size
が追加されました。これにより、map
、ビットマップ、アリーナの全ての領域がまとめて予約されるようになりました。ただし、この予約はあくまで仮想アドレス空間の予約であり、物理メモリのマッピングは遅延されます。MHeap_MapSpans
関数の追加:runtime·MHeap_MapSpans
という新しい関数が追加されました。この関数は、ヒープが拡張されるたびに呼び出され、MHeap.map
が必要とする領域のうち、まだ物理メモリにマッピングされていない部分をSysMap
でマッピングします。これにより、map
の物理メモリ割り当てが遅延され、実際に必要になったときにのみ行われるようになります。MHeap_SysAlloc
からの呼び出し:runtime·MHeap_SysAlloc
(ヒープに新しいアリーナを割り当てる関数)内で、runtime·MHeap_MapSpans
が呼び出されるようになりました。これにより、ヒープが成長するにつれて、map
も動的に拡張されるようになります。
この遅延割り当て戦略により、Goプログラムは起動時に不要な仮想アドレス空間を消費しなくなり、ulimit
による制限を受けにくくなります。また、GCの観点からは、ページテーブルが既知のアドレスにマッピングされることで、GCがメモリを走査する際のルックアップが効率化され、パフォーマンス向上に寄与する可能性があります。
コアとなるコードの変更箇所
このコミットでは、主に以下の4つのファイルが変更されています。
-
src/pkg/runtime/malloc.goc
:runtime·mallocinit
関数において、spans_size
変数が追加され、ヒープの初期化時にMHeap.map
、ビットマップ、アリーナの各領域のサイズ計算とSysReserve
呼び出しが変更されました。runtime·mheap->map
の初期化が、p
(予約された仮想アドレス空間の開始アドレス)からMSpan**
型として行われるようになりました。runtime·mheap->bitmap
とruntime·mheap->arena_start
のアドレス計算にspans_size
が考慮されるようになりました。runtime·MHeap_SysAlloc
関数内で、runtime·MHeap_MapSpans
が呼び出されるようになりました。
-
src/pkg/runtime/malloc.h
:MHeap
構造体内のmap
フィールドの定義がMSpan *map[1<<MHeapMap_Bits]
からMSpan** map
に変更され、spans_mapped
フィールド(map
が物理メモリにマッピングされたサイズを追跡)が追加されました。runtime·MHeap_MapSpans
関数のプロトタイプ宣言が追加されました。
-
src/pkg/runtime/mgc0.c
:runtime·MHeap_MapBits
関数内で、ROUND
マクロを使用してビットマップのサイズ計算がより簡潔になりました。これは直接的な機能変更ではありませんが、関連するコードのクリーンアップです。
-
src/pkg/runtime/mheap.c
:runtime·MHeap_MapSpans
関数が新しく追加されました。この関数は、MHeap.map
の物理メモリマッピングを遅延的に行うロジックを含んでいます。
コアとなるコードの解説
src/pkg/runtime/malloc.goc
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -323,7 +323,7 @@ void
runtime·mallocinit(void)
{
byte *p;
- uintptr arena_size, bitmap_size;
+ uintptr arena_size, bitmap_size, spans_size;
extern byte end[];
byte *want;
uintptr limit;
@@ -331,11 +331,13 @@ runtime·mallocinit(void)
p = nil;
arena_size = 0;
bitmap_size = 0;
-
+ spans_size = 0;
+
// for 64-bit build
USED(p);
USED(arena_size);
USED(bitmap_size);
+ USED(spans_size);
runtime·mallocinit
関数はGoランタイムのメモリヒープを初期化する部分です。ここでspans_size
という新しい変数が導入され、USED
マクロで未使用警告を抑制しています。
@@ -375,7 +377,8 @@ runtime·mallocinit(void)
// If this fails we fall back to the 32 bit memory mechanism
arena_size = MaxMem;
bitmap_size = arena_size / (sizeof(void*)*8/4);
- p = runtime·SysReserve((void*)(0x00c0ULL<<32), bitmap_size + arena_size);
+ spans_size = arena_size / PageSize * sizeof(runtime·mheap->map[0]);
+ p = runtime·SysReserve((void*)(0x00c0ULL<<32), bitmap_size + spans_size + arena_size);
}
if (p == nil) {
// On a 32-bit machine, we can't typically get away
@@ -397,11 +400,13 @@ runtime·mallocinit(void)
// of address space, which is probably too much in a 32-bit world.
bitmap_size = MaxArena32 / (sizeof(void*)*8/4);
arena_size = 512<<20;
- if(limit > 0 && arena_size+bitmap_size > limit) {
+ if(limit > 0 && arena_size+bitmap_size+spans_size > limit) {
bitmap_size = (limit / 9) & ~((1<<PageShift) - 1);
arena_size = bitmap_size * 8;
+ spans_size = arena_size / PageSize * sizeof(runtime·mheap->map[0]);
}
-
+
// SysReserve treats the address we ask for, end, as a hint,
// not as an absolute requirement. If we ask for the end
// of the data segment but the operating system requires
@@ -412,17 +417,19 @@ runtime·mallocinit(void)
// away from the running binary image and then round up
// to a MB boundary.
want = (byte*)(((uintptr)end + (1<<18) + (1<<20) - 1)&~((1<<20)-1));
- p = runtime·SysReserve(want, bitmap_size + arena_size);
+ p = runtime·SysReserve(want, bitmap_size + spans_size + arena_size);
if(p == nil)
runtime·throw("runtime: cannot reserve arena virtual address space");
if((uintptr)p & (((uintptr)1<<PageShift)-1))\
- runtime·printf("runtime: SysReserve returned unaligned address %p; asked for %p", p, bitmap_size+arena_size);\
+ runtime·printf("runtime: SysReserve returned unaligned address %p; asked for %p", p,\
+ bitmap_size+spans_size+arena_size);\
}\
if((uintptr)p & (((uintptr)1<<PageShift)-1))\
runtime·throw("runtime: SysReserve returned unaligned address");
-\truntime·mheap->bitmap = p;\
-\truntime·mheap->arena_start = p + bitmap_size;\
+\truntime·mheap->map = (MSpan**)p;\
+\truntime·mheap->bitmap = p + spans_size;\
+\truntime·mheap->arena_start = p + spans_size + bitmap_size;\
64-bitおよび32-bitビルドの両方で、spans_size
が計算され、runtime·SysReserve
の呼び出しにそのサイズが追加されています。これにより、map
、ビットマップ、アリーナの全ての領域がまとめて仮想アドレス空間として予約されます。
また、runtime·mheap->map
が予約された領域の先頭に配置され、bitmap
とarena_start
のアドレスがspans_size
分だけオフセットされるように変更されています。
@@ -461,6 +468,7 @@ runtime·MHeap_SysAlloc(MHeap *h, uintptr n)\
\truntime·SysMap(p, n);\
\th->arena_used += n;\
\truntime·MHeap_MapBits(h);\
+\t\truntime·MHeap_MapSpans(h);\
\tif(raceenabled)\
\t\truntime·racemapshadow(p, n);\
\treturn p;\
@@ -489,6 +497,7 @@ runtime·MHeap_SysAlloc(MHeap *h, uintptr n)\
\tif(h->arena_used > h->arena_end)\
\t\th->arena_end = h->arena_used;\
\truntime·MHeap_MapBits(h);\
+\t\truntime·MHeap_MapSpans(h);\
\tif(raceenabled)\
\t\truntime·racemapshadow(p, n);\
}\
runtime·MHeap_SysAlloc
は、ヒープに新しいアリーナ(メモリ領域)を割り当てる際に呼び出されます。ここで新しく追加されたruntime·MHeap_MapSpans(h)
が呼び出されており、ヒープが拡張されるたびにMHeap.map
の物理メモリマッピングも更新されるようになっています。
src/pkg/runtime/malloc.h
--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -411,7 +411,8 @@ struct MHeap
uint32 nspancap;
// span lookup
- MSpan *map[1<<MHeapMap_Bits];
+ MSpan** map;
+ uintptr spans_mapped;
// range of addresses we might see in the heap
byte *bitmap;
@@ -442,6 +443,7 @@ MSpan* runtime·MHeap_LookupMaybe(MHeap *h, void *v);\
void runtime·MGetSizeClassInfo(int32 sizeclass, uintptr *size, int32 *npages, int32 *nobj);\
void* runtime·MHeap_SysAlloc(MHeap *h, uintptr n);\
void runtime·MHeap_MapBits(MHeap *h);\
+void runtime·MHeap_MapSpans(MHeap *h);\
void runtime·MHeap_Scavenger(void);\
void* runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed);\
MHeap
構造体において、map
フィールドが固定サイズの配列からMSpan**
ポインタに変更され、spans_mapped
という新しいフィールドが追加されています。spans_mapped
は、map
が現在どれだけの物理メモリにマッピングされているかを追跡します。また、runtime·MHeap_MapSpans
関数のプロトタイプ宣言が追加されています。
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2403,7 +2403,7 @@ runtime·MHeap_MapBits(MHeap *h)
uintptr n;
n = (h->arena_used - h->arena_start) / wordsPerBitmapWord;
- n = (n+bitmapChunk-1) & ~(bitmapChunk-1);\
+ n = ROUND(n, bitmapChunk);\
if(h->bitmap_mapped >= n)\
return;\
runtime·MHeap_MapBits
関数内のビットマップサイズ計算がROUND
マクロを使用するように変更され、より簡潔になっています。これは直接的な機能変更ではありませんが、コードの可読性と保守性を向上させます。
src/pkg/runtime/mheap.c
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -65,6 +65,24 @@ runtime·MHeap_Init(MHeap *h, void *(*alloc)(uintptr))\
\t\truntime·MCentral_Init(&h->central[i], i);\
}\
\
+void\
+runtime·MHeap_MapSpans(MHeap *h)\
+{\
+\tuintptr n;\
+\n\
+\t// Map spans array, PageSize at a time.\
+\tn = (uintptr)h->arena_used;\
+\tif(sizeof(void*) == 8)\
+\t\tn -= (uintptr)h->arena_start;\
+\t// Coalescing code reads spans past the end of mapped arena, thus +1.\
+\tn = (n / PageSize + 1) * sizeof(h->map[0]);\
+\tn = ROUND(n, PageSize);\
+\tif(h->spans_mapped >= n)\
+\t\treturn;\
+\truntime·SysMap((byte*)h->map + h->spans_mapped, n - h->spans_mapped);\
+\th->spans_mapped = n;\
+}\
+\n\
// Allocate a new span of npage pages from the heap\
// and record its size class in the HeapMap and HeapMapCache.\
MSpan*\
runtime·MHeap_MapSpans
関数が新しく追加されました。この関数は、MHeap.map
が物理メモリにマッピングされるべきサイズn
を計算し、現在マッピングされているサイズh->spans_mapped
と比較します。もしn
がh->spans_mapped
よりも大きい場合、SysMap
を呼び出して、まだマッピングされていない追加の領域を物理メモリにマッピングします。これにより、map
の物理メモリ割り当てが遅延的に行われ、必要な分だけが確保されるようになります。
関連リンク
- Go CL (Code Review) 9791044: https://golang.org/cl/9791044
参考にした情報源リンク
- Goのメモリ管理とGCに関するStack Overflowの議論: https://stackoverflow.com/questions/tagged/go-memory-management
- Goのメモリ管理の内部構造に関する記事: https://mtardy.com/posts/go-memory-management
- Goのメモリ割り当てとGCの仕組みに関する解説: https://go101.org/article/memory-management.html
- Go 1.19で導入された
GOMEMLIMIT
に関する公式ドキュメント: https://go.dev/doc/gc-guide - GoのメモリリークとGCに関するMedium記事: https://medium.com/@pangyoalto/go-memory-leak-and-garbage-collection-d0e7b7e7e7e7
- GoのGCの仕組みに関するLINE Engineeringの記事: https://engineering.linecorp.com/ja/blog/go-garbage-collection-deep-dive/
- Goのメモリプロファイリングに関する記事: https://medium.com/a-journey-with-go/go-memory-profiling-a-practical-guide-d59204075027
- GitHub上のGoのGC関連Issue (例: #64682): https://github.com/golang/go/issues/64682
ulimit
に関するStack Overflowの議論: https://stackoverflow.com/questions/tagged/ulimit