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

[インデックス 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という大きなメモリ領域の事前割り当てが、特定の環境下で問題を引き起こしていたことがあります。

  1. ulimitとの競合: ulimitはUnix系OSにおけるプロセスが利用できるリソース(メモリ、ファイルディスクリプタなど)を制限する機能です。特にRLIMIT_DATAのようなデータセグメントのサイズを制限する設定がされている場合、Goランタイムが起動時に256MBものメモリを確保しようとすると、このulimit制限に抵触し、プログラムが起動できない、あるいは異常終了する問題が発生していました。Goのメモリ管理はmmap()を使用してOSから大きなメモリ領域を要求するため、従来のbrk()で管理されるデータセグメントのulimitとは直接関係しないことが多いですが、RLIMIT_AS(アドレス空間の制限)のようなより広範な制限が適用される場合には影響を受けます。このコミットは、この起動時の大きな割り当てを遅延させることで、ulimitによる起動失敗のリスクを軽減します。

  2. GCの最適化の可能性: コミットメッセージには「GCにおける不要なメモリアクセスを排除する可能性」も言及されています。ページテーブルが既知のアドレスにマッピングされることで、GCがメモリを走査する際に、ページテーブルの情報をより効率的に利用できるようになることが示唆されています。これにより、GCのパフォーマンス向上やレイテンシの削減に寄与する可能性があります。

  3. 関連するIssue:

    • Update #5049: Web検索では直接的な「GC issue 5049」は見つかりませんでしたが、Goのメモリ管理やGCに関する一般的な課題、特にメモリリークと誤解されがちな挙動(ゴルーチンのハング、グローバルキャッシュの肥大化など)が議論されることがあります。このコミットは、起動時のメモリフットプリントを減らすことで、全体的なメモリ管理の健全性に貢献します。
    • Update #5236: Web検索ではGoに関連する「issue 5236」は見つかりませんでした。これは内部的なIssueトラッカーの番号であるか、あるいは公開されていない情報である可能性があります。しかし、文脈から判断すると、これも起動時のメモリ割り当てやulimitに関連する問題であったと推測されます。

これらの問題に対処するため、Goランタイムのメモリ管理メカニズム、特にページテーブルの割り当て戦略が見直され、遅延割り当てが導入されました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. Goランタイムのメモリ管理:

    • Goは独自のメモリマネージャを持ち、OSから大きな仮想メモリ領域をmmap()システムコール(Unix系)やVirtualAlloc(Windows)などを用いて取得します。これは従来のC/C++プログラムがbrk()システムコールでデータセグメントを拡張するのとは異なります。
    • Goのヒープは、これらのOSから取得した仮想メモリ領域(アリーナ)内に構築されます。
    • Goランタイムは、ヒープ内のメモリブロックを管理するために、内部的に様々なデータ構造を使用します。これには、アリーナ内の各ページがどのように使用されているかを追跡する「ページテーブル」や、メモリブロックの割り当て状態を管理する「ビットマップ」などが含まれます。
  2. ページテーブル (Page Table):

    • オペレーティングシステム(OS)の仮想記憶管理において、仮想アドレスを物理アドレスに変換するために使用されるデータ構造です。各プロセスは独自のページテーブルを持ちます。
    • Goランタイムの文脈では、Goが管理するヒープ領域内の仮想アドレス空間を、より効率的に管理するための内部的なマッピング構造を指すことがあります。このコミットで言及されている「ページテーブル」は、Goランタイムがヒープ内のメモリブロック(スパン)を管理するために使用するMSpan構造体へのポインタの配列(MHeap.map)を指しています。これは、特定のアドレスがどのMSpanに属するかを高速にルックアップするために使われます。
  3. ulimit:

    • Unix系OSのシェルコマンドで、ユーザーが起動するプロセスが利用できるシステムリソース(CPU時間、ファイルサイズ、メモリなど)に制限を設定するために使用されます。
    • RLIMIT_DATA: プロセスのデータセグメントの最大サイズを制限します。
    • RLIMIT_AS (Address Space Limit): プロセスの仮想アドレス空間の最大サイズを制限します。Goのようにmmap()を多用するアプリケーションでは、RLIMIT_DATAよりもRLIMIT_ASが影響を与える可能性が高いです。
  4. SysAlloc, SysReserve, SysMap:

    • これらはGoランタイムがOSからメモリを要求する際に使用する低レベルの関数です。
    • SysReserve: 仮想アドレス空間を予約しますが、物理メモリは割り当てません。これは、将来的に使用するアドレス範囲を確保するもので、コミットメッセージの「256MB memory allocation at startup」が指すのは、この予約プロセスの一部であったと考えられます。
    • SysMap: 予約された仮想アドレス空間に物理メモリをマッピングします。実際にメモリが使用可能になるのはこの段階です。
    • SysAlloc: SysReserveSysMapを組み合わせて、仮想アドレス空間の予約と物理メモリのマッピングを同時に行います。
  5. Goのガベージコレクション (GC):

    • GoのGCは、不要になったメモリを自動的に解放する仕組みです。Go 1.5以降は並行(concurrent)かつ非移動(non-moving)のマーク&スイープ方式を採用しており、STW(Stop-The-World)時間を最小限に抑えるように設計されています。
    • GCはヒープ全体を走査し、到達可能なオブジェクトをマークし、マークされなかったオブジェクトを解放します。このプロセスにおいて、メモリのレイアウトや内部データ構造(ページテーブルなど)へのアクセス効率がGCのパフォーマンスに影響を与えます。

技術的詳細

このコミットの核心は、Goランタイムのヒープ管理構造であるMHeapmapフィールド(ページテーブルに相当)の割り当て方法を、起動時の事前割り当てから遅延割り当てへと変更した点にあります。

以前のGoランタイムでは、MHeap.mapは起動時に固定サイズ(例えば256MB)の仮想アドレス空間をSysReserveで確保していました。これは、将来的にヒープが拡張された際に、mapがヒープ全体をカバーできるようにするためでした。しかし、この事前割り当ては、たとえ実際にその領域が使用されなくても、仮想アドレス空間を消費し、特にulimitが厳しく設定されている環境では問題となっていました。

この変更により、MHeap.mapは起動時には最小限の領域しか確保せず、ヒープが実際に拡張され、より大きなmap領域が必要になった場合にのみ、SysMapを呼び出して物理メモリをマッピングし、mapのサイズを動的に拡張するようになりました。

具体的には、以下の点が変更されました。

  1. MHeap.mapの型変更: MSpan *map[1<<MHeapMap_Bits]という固定サイズの配列から、MSpan** mapというポインタ型に変更されました。これにより、map自体が動的に割り当てられるようになりました。
  2. spans_sizeの導入: arena_size(ヒープ領域のサイズ)とbitmap_size(ビットマップのサイズ)に加えて、spans_sizeという変数が導入されました。これはMHeap.mapが占めるべきサイズを計算するために使用されます。
  3. SysReserveの引数変更: runtime·SysReserveの呼び出しにおいて、予約するメモリサイズにspans_sizeが追加されました。これにより、map、ビットマップ、アリーナの全ての領域がまとめて予約されるようになりました。ただし、この予約はあくまで仮想アドレス空間の予約であり、物理メモリのマッピングは遅延されます。
  4. MHeap_MapSpans関数の追加: runtime·MHeap_MapSpansという新しい関数が追加されました。この関数は、ヒープが拡張されるたびに呼び出され、MHeap.mapが必要とする領域のうち、まだ物理メモリにマッピングされていない部分をSysMapでマッピングします。これにより、mapの物理メモリ割り当てが遅延され、実際に必要になったときにのみ行われるようになります。
  5. MHeap_SysAllocからの呼び出し: runtime·MHeap_SysAlloc(ヒープに新しいアリーナを割り当てる関数)内で、runtime·MHeap_MapSpansが呼び出されるようになりました。これにより、ヒープが成長するにつれて、mapも動的に拡張されるようになります。

この遅延割り当て戦略により、Goプログラムは起動時に不要な仮想アドレス空間を消費しなくなり、ulimitによる制限を受けにくくなります。また、GCの観点からは、ページテーブルが既知のアドレスにマッピングされることで、GCがメモリを走査する際のルックアップが効率化され、パフォーマンス向上に寄与する可能性があります。

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

このコミットでは、主に以下の4つのファイルが変更されています。

  1. src/pkg/runtime/malloc.goc:

    • runtime·mallocinit関数において、spans_size変数が追加され、ヒープの初期化時にMHeap.map、ビットマップ、アリーナの各領域のサイズ計算とSysReserve呼び出しが変更されました。
    • runtime·mheap->mapの初期化が、p(予約された仮想アドレス空間の開始アドレス)からMSpan**型として行われるようになりました。
    • runtime·mheap->bitmapruntime·mheap->arena_startのアドレス計算にspans_sizeが考慮されるようになりました。
    • runtime·MHeap_SysAlloc関数内で、runtime·MHeap_MapSpansが呼び出されるようになりました。
  2. src/pkg/runtime/malloc.h:

    • MHeap構造体内のmapフィールドの定義がMSpan *map[1<<MHeapMap_Bits]からMSpan** mapに変更され、spans_mappedフィールド(mapが物理メモリにマッピングされたサイズを追跡)が追加されました。
    • runtime·MHeap_MapSpans関数のプロトタイプ宣言が追加されました。
  3. src/pkg/runtime/mgc0.c:

    • runtime·MHeap_MapBits関数内で、ROUNDマクロを使用してビットマップのサイズ計算がより簡潔になりました。これは直接的な機能変更ではありませんが、関連するコードのクリーンアップです。
  4. 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が予約された領域の先頭に配置され、bitmaparena_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と比較します。もしnh->spans_mappedよりも大きい場合、SysMapを呼び出して、まだマッピングされていない追加の領域を物理メモリにマッピングします。これにより、mapの物理メモリ割り当てが遅延的に行われ、必要な分だけが確保されるようになります。

関連リンク

参考にした情報源リンク