[インデックス 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点が重要な要素となっています。
- 先行コミット「9791044: runtime: allocate page table lazily」との依存関係: このコミットは、ページテーブルの割り当てを遅延させる先行コミットに依存しています。ページテーブルがGoのヒープ(
mheap
が管理する領域)から切り離され、必要に応じて物理メモリがコミットされるようになったことで、mheap
自体のサイズが大幅に小さくなりました。 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·mheap
は MHeap
構造体へのポインタとして宣言されていました。
MHeap *runtime·mheap;
この場合、runtime·mheap
を介して MHeap
構造体のメンバーにアクセスする際には、runtime·mheap->member
のようにポインタのデリファレンス (->
) が必要でした。
このコミットでは、runtime·mheap
が MHeap
構造体そのものとして静的に宣言されるように変更されました。
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
というグローバル変数の扱い方です。
-
MHeap *runtime·mheap;
からMHeap runtime·mheap;
への変更:- 変更前は、
runtime·mheap
はMHeap
構造体へのポインタでした。これは、mheap
構造体自体がヒープ上に動的に割り当てられ、そのアドレスがruntime·mheap
に格納されることを意味します。 - 変更後は、
runtime·mheap
はMHeap
構造体そのものとして宣言されます。これは、mheap
構造体がプログラムのデータセグメントに静的に割り当てられることを意味します。つまり、プログラムの実行開始時にそのメモリ領域が確保され、そのアドレスは固定されます。
- 変更前は、
-
runtime·mheap->member
からruntime·mheap.member
または&runtime·mheap
への変更:runtime·mheap
がポインタであった場合、そのメンバーにアクセスするには->
演算子(例:runtime·mheap->cachealloc
)を使用して、ポインタが指すアドレスの値をデリファレンス(間接参照の解決)する必要がありました。runtime·mheap
が構造体そのものになったことで、メンバーへのアクセスは.
演算子(例:runtime·mheap.cachealloc
)を使って直接行えるようになります。- また、
runtime·MHeap_Alloc
やruntime·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
- このコミットは、ページテーブルをヒープから移動させ、
参考にした情報源リンク
- Go runtime mheap static allocation:
- https://sobyte.net/post/2022-03/go-memory-management/
- https://github.com/golang/go/blob/master/src/runtime/mheap.go
- https://go.dev/src/runtime/mheap.go
- https://github.com/golang/go/issues/14959
- https://go101.org/article/memory-management.html
- https://deepu.tech/go-memory-management/
- https://gitconnected.com/go-memory-management-deep-dive/
- Go runtime page table lazy allocation:
- https://www.datadoghq.com/blog/go-memory-management/
- https://cloud.google.com/blog/products/gcp/go-memory-management-and-performance-optimizations
- https://povilasv.me/go-memory-management/
- https://medium.com/a-journey-with-go/go-memory-management-tcmalloc-and-the-go-runtime-502464925f7f
- https://github.com/golang/go/issues/14959