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

[インデックス 16425] ファイルの概要

このコミットは、Goランタイムのメモリヒープ (mheap) の割り当て方法を、動的なヒープ割り当てから静的な割り当てへと変更するものです。これにより、ヒープへのアクセス時に発生していた不要な間接参照が削除され、パフォーマンスの向上が図られています。この変更は、先行するコミット「9791044: runtime: allocate page table lazily」によってページテーブルがヒープから移動されたことで可能になりました。

コミット

  • コミットハッシュ: 8bbb08533dab0dcf627db0b76ba65c3fb9b1d682
  • 作者: Dmitriy Vyukov dvyukov@google.com
  • 日付: Tue May 28 22:14:47 2013 +0400

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/8bbb08533dab0dcf627db0b76ba65c3fb9b1d682

元コミット内容

runtime: make mheap statically allocated again
This depends on: 9791044: runtime: allocate page table lazily
Once page table is moved out of heap, the heap becomes small.
This removes unnecessary dereferences during heap access.
No logical changes.

R=golang-dev, khr
CC=golang-dev
https://golang.org/cl/9802043

変更の背景

このコミットの主な背景には、Goランタイムのメモリ管理における効率化と最適化があります。特に、以下の2点が重要な要素となっています。

  1. 先行コミット「9791044: runtime: allocate page table lazily」との依存関係: このコミットは、ページテーブルの割り当てを遅延させる先行コミットに依存しています。ページテーブルがGoのヒープ(mheapが管理する領域)から切り離され、必要に応じて物理メモリがコミットされるようになったことで、mheap自体のサイズが大幅に小さくなりました。
  2. mheapアクセス時の間接参照の削減: mheapがポインタを介してアクセスされる構造であったため、メモリヒープの操作のたびにポインタのデリファレンス(間接参照の解決)が発生していました。これは、特に頻繁にアクセスされるmheapのような構造体にとって、わずかながらもパフォーマンスのオーバーヘッドとなっていました。mheapが小さくなったことで、これを静的に割り当てることが現実的になり、間接参照を排除することでアクセスを高速化する機会が生まれました。

この変更は、Goランタイムのメモリ管理のコア部分におけるマイクロ最適化であり、論理的な動作の変更を伴わず、純粋にパフォーマンスの向上を目的としています。

前提知識の解説

Goランタイムのメモリ管理 (TCMallocベース)

Go言語のランタイムは、GoogleのTCMalloc (Thread-Caching Malloc) に影響を受けたメモリ管理システムを採用しています。このシステムは、並行処理環境でのメモリ割り当ての効率を高めるために設計されています。主要なコンポーネントは以下の通りです。

  • mcache (Per-P Cache): 各論理プロセッサ (P) に紐付けられたスレッドローカルなキャッシュです。Goルーチンが小さなオブジェクトを割り当てる際に、グローバルなロックを必要とせずに高速にメモリを供給します。これにより、ロックの競合が減少し、並行性が向上します。
  • mcentral (Central List): mcacheがメモリを使い果たした際に、mheapからメモリブロック(スパン)を取得し、mcacheに供給する役割を担います。また、mcacheから返却されたメモリブロックを管理します。
  • mheap (Global Heap): Goランタイムが管理する全てのヒープメモリの最上位の管理者です。オペレーティングシステムからメモリを要求し、それをmcentralに供給するための大きなメモリブロック(ページ)を管理します。mheapは、Goプログラムが使用する動的に割り当てられるメモリ(ヒープ)全体を抽象化します。

mheapの役割と静的割り当ての重要性

mheapは、Goのメモリ管理において非常に重要な役割を担っています。Goプログラムがメモリを要求すると、最終的にはmheapが管理する領域から割り当てられます。mheap自体が静的に割り当てられる(つまり、プログラムの開始時に固定のアドレスに配置される)ことには、いくつかの利点があります。

  • アドレスの固定化: mheapのアドレスが固定されることで、その構造体へのアクセスがポインタのデリファレンスを必要とせず、直接行えるようになります。これにより、CPUのキャッシュ効率が向上し、メモリアクセスのレイテンシが削減されます。
  • GCからの独立: mheapが静的に割り当てられることで、Goのガベージコレクタ (GC) の管理対象外となります。GCはヒープ上のオブジェクトをスキャンし、不要なものを解放しますが、mheap自体がGCの対象とならないことで、GCの複雑性が軽減され、GCサイクル中のmheapへのアクセスがより安定します。

ページテーブルの遅延割り当て (Lazy Allocation of Page Tables)

Goランタイムは、メモリを効率的に利用するために、ページテーブルの遅延割り当て戦略を採用しています。これは、先行するコミット「9791044: runtime: allocate page table lazily」で導入されたものです。

  • 仮想メモリの予約: Goランタイムは、ヒープのために大量の仮想アドレス空間をオペレーティングシステムから予約します。この時点では、物理メモリは割り当てられません。
  • 物理メモリの遅延コミット: 実際にGoプログラムがその仮想アドレス空間内のメモリにアクセスしようとしたときに初めて、オペレーティングシステムが物理メモリを割り当て、その仮想アドレスにマッピングします。このプロセスは「ページフォルト」を介して行われます。
  • 効率性: この戦略により、Goプログラムは大きな潜在的なメモリ空間を持つことができますが、実際に使用される物理メモリの量は、必要になるまで最小限に抑えられます。これにより、メモリフットプリントが削減され、システムリソースの効率的な利用が可能になります。

この遅延割り当てにより、mheapが管理する必要のある「ページテーブル」のデータ構造がヒープから切り離され、mheap自体のサイズが小さくなったため、今回のコミットでmheapを静的に割り当てることが可能になりました。

技術的詳細

このコミットの核心は、Goランタイムのグローバル変数 runtime·mheap の型定義の変更と、それに伴うアクセス方法の変更です。

変更前は、runtime·mheapMHeap 構造体へのポインタとして宣言されていました。

MHeap *runtime·mheap;

この場合、runtime·mheap を介して MHeap 構造体のメンバーにアクセスする際には、runtime·mheap->member のようにポインタのデリファレンス (->) が必要でした。

このコミットでは、runtime·mheapMHeap 構造体そのものとして静的に宣言されるように変更されました。

MHeap runtime·mheap;

これにより、runtime·mheap のメンバーにアクセスする際には、runtime·mheap.member のように直接構造体メンバーにアクセスできるようになります。また、MHeap 構造体へのポインタを引数として取る関数(例: runtime·MHeap_Alloc)には、&runtime·mheap のように runtime·mheap のアドレスを渡す形に変更されています。

この変更により、mheapへのアクセスパスからポインタのデリファレンスが一つ削減されます。これは、CPUがメモリからデータを読み込む際に、ポインタを解決するための追加のメモリ参照が不要になることを意味します。現代のCPUでは、メモリ参照は非常にコストの高い操作であり、特に頻繁に行われる場合にはパフォーマンスに大きな影響を与えます。間接参照の削減は、CPUのキャッシュヒット率の向上にも寄与し、全体的なメモリ管理の効率を高めます。

この変更は、Goランタイムのメモリ管理の内部実装に関するものであり、Go言語のユーザーが直接意識するようなAPIの変更や振る舞いの変更はありません。純粋にランタイムのパフォーマンス最適化を目的としたものです。

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

このコミットでは、runtime·mheap の宣言と、それを使用している箇所が広範囲にわたって変更されています。

src/pkg/runtime/malloc.h:

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -433,7 +433,7 @@ struct MHeap
 	FixAlloc spanalloc;	// allocator for Span*
 	FixAlloc cachealloc;	// allocator for MCache*
 };
-extern MHeap *runtime·mheap;
+extern MHeap runtime·mheap;
 
 void	runtime·MHeap_Init(MHeap *h, void *(*allocator)(uintptr));
 MSpan*	runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32 zeroed);

runtime·mheap の型が MHeap * (ポインタ) から MHeap (構造体そのもの) に変更されています。

src/pkg/runtime/malloc.goc:

runtime·mheap の宣言がポインタから構造体に変更され、それに伴い runtime·mheap-> のアクセスが runtime·mheap. または &runtime·mheap に変更されています。

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -14,7 +14,7 @@ package runtime
 #include "typekind.h"
 #include "race.h"
 
-MHeap *runtime·mheap;
+MHeap runtime·mheap;
 
 int32	runtime·checking;
 
@@ -81,7 +81,7 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
 		npages = size >> PageShift;
 		if((size & PageMask) != 0)
 			npages++;
-		s = runtime·MHeap_Alloc(runtime·mheap, npages, 0, 1, zeroed);
+		s = runtime·MHeap_Alloc(&runtime·mheap, npages, 0, 1, zeroed);
 		if(s == nil)
 			runtime·throw("out of memory");
 		size = npages<<PageShift;
@@ -95,9 +95,9 @@ runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
 
 	if (sizeof(void*) == 4 && c->local_total_alloc >= (1<<30)) {
 		// purge cache stats to prevent overflow
-		runtime·lock(runtime·mheap);
+		runtime·lock(&runtime·mheap);
 		runtime·purgecachedstats(c);
-		runtime·unlock(runtime·mheap);
+		runtime·unlock(&runtime·mheap);
 	}
 
 	if(!(flag & FlagNoGC))
@@ -181,7 +181,7 @@ runtime·free(void *v)
 		// they might coalesce v into other spans and change the bitmap further.
 		runtime·markfreed(v, size);
 		runtime·unmarkspan(v, 1<<PageShift);
-		runtime·MHeap_Free(runtime·mheap, s, 1);
+		runtime·MHeap_Free(&runtime·mheap, s, 1);
 	} else {
 		// Small object.
 		size = runtime·class_to_size[sizeclass];
@@ -211,12 +211,12 @@ runtime·mlookup(void *v, byte **base, uintptr *size, MSpan **sp)
 	m->mcache->local_nlookup++;
 	if (sizeof(void*) == 4 && m->mcache->local_nlookup >= (1<<30)) {
 		// purge cache stats to prevent overflow
-		runtime·lock(runtime·mheap);
+		runtime·lock(&runtime·mheap);
 		runtime·purgecachedstats(m->mcache);
-		runtime·unlock(runtime·mheap);
+		runtime·unlock(&runtime·mheap);
 	}
 
-	s = runtime·MHeap_LookupMaybe(runtime·mheap, v);
+	s = runtime·MHeap_LookupMaybe(&runtime·mheap, v);
 	if(sp)
 		*sp = s;
 	if(s == nil) {
@@ -260,11 +260,11 @@ runtime·allocmcache(void)
 	intgo rate;
 	MCache *c;
 
-	runtime·lock(runtime·mheap);
-	c = runtime·FixAlloc_Alloc(&runtime·mheap->cachealloc);
-	mstats.mcache_inuse = runtime·mheap->cachealloc.inuse;
-	mstats.mcache_sys = runtime·mheap->cachealloc.sys;
-	runtime·unlock(runtime·mheap);
+	runtime·lock(&runtime·mheap);
+	c = runtime·FixAlloc_Alloc(&runtime·mheap.cachealloc);
+	mstats.mcache_inuse = runtime·mheap.cachealloc.inuse;
+	mstats.mcache_sys = runtime·mheap.cachealloc.sys;
+	runtime·unlock(&runtime·mheap);
 	runtime·memclr((byte*)c, sizeof(*c));
 
 	// Set first allocation sample size.
@@ -281,10 +281,10 @@ void
 runtime·freemcache(MCache *c)
 {
 	runtime·MCache_ReleaseAll(c);
-	runtime·lock(runtime·mheap);
+	runtime·lock(&runtime·mheap);
 	runtime·purgecachedstats(c);
-	runtime·FixAlloc_Free(&runtime·mheap->cachealloc, c);
-	runtime·unlock(runtime·mheap);
+	runtime·FixAlloc_Free(&runtime·mheap.cachealloc, c);
+	runtime·unlock(&runtime·mheap);
 }
 
 void
@@ -339,9 +339,6 @@ runtime·mallocinit(void)
 	USED(bitmap_size);
 	USED(spans_size);
 
-	if((runtime·mheap = runtime·SysAlloc(sizeof(*runtime·mheap))) == nil)
-		runtime·throw("runtime: cannot allocate heap metadata");
-
 	runtime·InitSizes();
 
 	// limit = runtime·memlimit();
@@ -377,7 +374,7 @@ 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);
-		spans_size = arena_size / PageSize * sizeof(runtime·mheap->map[0]);
+		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) {
@@ -400,11 +397,11 @@ 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;
-		spans_size = MaxArena32 / PageSize * sizeof(runtime·mheap->map[0]);
+		spans_size = MaxArena32 / PageSize * sizeof(runtime·mheap.map[0]);
 		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]);
+			spans_size = arena_size / PageSize * sizeof(runtime·mheap.map[0]);
 		}
 
 		// SysReserve treats the address we ask for, end, as a hint,
@@ -427,14 +424,14 @@ runtime·mallocinit(void)
 	if((uintptr)p & (((uintptr)1<<PageShift)-1))
 		runtime·throw("runtime: SysReserve returned unaligned address");
 
-	runtime·mheap->map = (MSpan**)p;
-	runtime·mheap->bitmap = p + spans_size;
-	runtime·mheap->arena_start = p + spans_size + bitmap_size;
-	runtime·mheap->arena_used = runtime·mheap->arena_start;
-	runtime·mheap->arena_end = runtime·mheap->arena_start + arena_size;
+	runtime·mheap.map = (MSpan**)p;
+	runtime·mheap.bitmap = p + spans_size;
+	runtime·mheap.arena_start = p + spans_size + bitmap_size;
+	runtime·mheap.arena_used = runtime·mheap.arena_start;
+	runtime·mheap.arena_end = runtime·mheap.arena_start + arena_size;
 
 	// Initialize the rest of the allocator.	
-	runtime·MHeap_Init(runtime·mheap, runtime·SysAlloc);
+	runtime·MHeap_Init(&runtime·mheap, runtime·SysAlloc);
 	m->mcache = runtime·allocmcache();
 
 	// See if it works.
@@ -534,8 +531,8 @@ runtime·settype_flush(M *mp, bool sysalloc)
 		// (Manually inlined copy of runtime·MHeap_Lookup)
 		p = (uintptr)v>>PageShift;
 		if(sizeof(void*) == 8)
-			p -= (uintptr)runtime·mheap->arena_start >> PageShift;
-		s = runtime·mheap->map[p];
+			p -= (uintptr)runtime·mheap.arena_start >> PageShift;
+		s = runtime·mheap.map[p];
 
 		if(s->sizeclass == 0) {
 			s->types.compression = MTypes_Single;
@@ -652,7 +649,7 @@ runtime·settype(void *v, uintptr t)
 	}
 
 	if(DebugTypeAtBlockEnd) {
-		s = runtime·MHeap_Lookup(runtime·mheap, v);
+		s = runtime·MHeap_Lookup(&runtime·mheap, v);
 		*(uintptr*)((uintptr)v+s->elemsize-sizeof(uintptr)) = t;
 	}
 }
@@ -691,7 +688,7 @@ runtime·gettype(void *v)
 	uintptr t, ofs;
 	byte *data;
 
-	s = runtime·MHeap_LookupMaybe(runtime·mheap, v);
+	s = runtime·MHeap_LookupMaybe(&runtime·mheap, v);
 	if(s != nil) {
 		t = 0;
 		switch(s->types.compression) {

同様の変更が、src/pkg/runtime/mcache.c, src/pkg/runtime/mcentral.c, src/pkg/runtime/mgc0.c, src/pkg/runtime/mheap.c, src/pkg/runtime/panic.c, src/pkg/runtime/race.c の各ファイルで行われています。

コアとなるコードの解説

このコミットのコアとなる変更は、runtime·mheap というグローバル変数の扱い方です。

  1. MHeap *runtime·mheap; から MHeap runtime·mheap; への変更:

    • 変更前は、runtime·mheapMHeap 構造体へのポインタでした。これは、mheap 構造体自体がヒープ上に動的に割り当てられ、そのアドレスが runtime·mheap に格納されることを意味します。
    • 変更後は、runtime·mheapMHeap 構造体そのものとして宣言されます。これは、mheap 構造体がプログラムのデータセグメントに静的に割り当てられることを意味します。つまり、プログラムの実行開始時にそのメモリ領域が確保され、そのアドレスは固定されます。
  2. runtime·mheap->member から runtime·mheap.member または &runtime·mheap への変更:

    • runtime·mheap がポインタであった場合、そのメンバーにアクセスするには -> 演算子(例: runtime·mheap->cachealloc)を使用して、ポインタが指すアドレスの値をデリファレンス(間接参照の解決)する必要がありました。
    • runtime·mheap が構造体そのものになったことで、メンバーへのアクセスは . 演算子(例: runtime·mheap.cachealloc)を使って直接行えるようになります。
    • また、runtime·MHeap_Allocruntime·lock のように MHeap 構造体へのポインタを引数として受け取る関数に対しては、runtime·mheap のアドレスを明示的に渡すために &runtime·mheap と記述するよう変更されています。

この変更がもたらす効果:

  • 間接参照の削減: 最も重要な効果は、mheap 構造体へのアクセス時に発生していた不要なポインタのデリファレンスがなくなることです。CPUがメモリからデータを読み込む際、ポインタを介してアクセスする場合、まずポインタの値(アドレス)を読み込み、次にそのアドレスが指す場所から実際のデータを読み込むという2段階のプロセスが必要です。静的割り当てにすることで、この最初の段階が不要になり、直接データにアクセスできるようになります。
  • CPUキャッシュ効率の向上: 間接参照が減ることで、CPUのキャッシュミスが減少する可能性があります。mheap のデータが常に同じ固定されたメモリ位置にあるため、CPUはより効率的にそのデータをキャッシュに保持し、高速にアクセスできます。これは、メモリ管理のような頻繁にアクセスされるコードパスにおいて、特に顕著なパフォーマンス向上をもたらします。
  • コードの簡素化: ポインタのデリファレンスが不要になることで、コードの記述がわずかに簡素化されます。

この変更は「論理的な変更なし」とコミットメッセージにある通り、Goランタイムのメモリ管理の振る舞いを変更するものではなく、既存の機能をより効率的に実行するための内部的な最適化です。先行するページテーブルの遅延割り当てによって mheap のサイズが小さくなったことが、この静的割り当てへの移行を可能にしました。

関連リンク

  • 先行コミット: 9791044: runtime: allocate page table lazily
    • このコミットは、ページテーブルをヒープから移動させ、mheapのサイズを小さくすることで、今回のmheapの静的割り当てを可能にしました。
    • GitHub URL: https://github.com/golang/go/commit/9791044

参考にした情報源リンク