[インデックス 18378] ファイルの概要
このコミットは、Go言語のランタイムにおけるメモリ管理の中核を担う src/pkg/runtime/malloc.goc
ファイルに対する変更です。このファイルは、Goプログラムが実行時にヒープメモリをどのように確保し、管理するかを定義しています。具体的には、メモリのアリーナ(大きな連続したメモリ領域)の予約、ビットマップ(メモリ使用状況の追跡)、スパン(メモリブロックの管理)といった、ガベージコレクションと密接に関連する低レベルのメモリ割り当てロジックが含まれています。
コミット
Goランタイムが将来的に8KBのページサイズをサポートするための準備として、ヒープが常にPageSize
境界にアラインされるように変更されました。これにより、runtime·SysReserve
関数が返すアドレスが、OSのページサイズよりも大きいGoランタイムのPageSize
にアラインされていない場合でも、適切に処理できるようになります。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/327e431057f0e367e7287c3cc4326196f53218cb
元コミット内容
runtime: prepare for 8K pages
Ensure than heap is PageSize aligned.
LGTM=iant
R=iant, dave, gobot
CC=golang-codereviews
https://golang.org/cl/56630043
変更の背景
この変更の主な背景は、Goランタイムが将来的に8KBのメモリページサイズをサポートするための準備です。従来のシステムでは、OSのページサイズ(一般的には4KB)がGoランタイムのPageSize
と一致していることが多かったですが、より大きなページサイズ(例えば8KB)を使用するシステムや、Goランタイムが内部的に異なるPageSize
を扱う場合に問題が発生する可能性がありました。
特に、runtime·SysReserve
のようなシステムコールは、OSが管理する物理メモリページに基づいてメモリを予約します。この予約されたメモリの開始アドレスは、OSのページサイズにはアラインされますが、Goランタイムが内部的に定義するPageSize
(これはOSのページサイズよりも大きい場合がある)にはアラインされない可能性があります。
もしヒープの開始アドレスがGoランタイムのPageSize
にアラインされていない場合、メモリ管理システム(特にガベージコレクタやアロケータ)が正しく機能しないか、パフォーマンスが低下する可能性があります。このコミットは、このような潜在的なアラインメントの問題を事前に解決し、将来のページサイズ変更に備えることを目的としています。
前提知識の解説
1. メモリページとページサイズ (PageSize)
コンピュータのメモリは、OSによって「ページ」と呼ばれる固定サイズのブロックに分割されて管理されます。これは仮想記憶システムの中核であり、物理メモリと仮想メモリのマッピングを可能にします。一般的なOSのページサイズは4KBですが、システムによっては8KB、16KB、64KBなど、より大きなページサイズを使用することもあります。
Goランタイムもまた、独自のメモリ管理のために内部的な「ページサイズ」を定義しています。このGoランタイムのPageSize
は、OSのページサイズと同じであるとは限りません。Goのメモリ管理では、このPageSize
を基準として、メモリブロックの割り当てや解放、ガベージコレクションのマーク処理などが行われます。
2. メモリのアラインメント (Memory Alignment)
メモリのアラインメントとは、データがメモリ上で特定のバイト境界に配置されることを保証する概念です。例えば、「4バイトアラインメント」とは、データの開始アドレスが4の倍数になるように配置されることを意味します。
CPUは、アラインされたデータにアクセスする方が、アラインされていないデータにアクセスするよりも効率的です。特に、ページサイズの境界にアラインされたメモリ領域は、OSやハードウェアが効率的に管理・アクセスするために重要です。Goランタイムのヒープも、その効率的な運用のためには、GoランタイムのPageSize
にアラインされている必要があります。
3. runtime·SysReserve
runtime·SysReserve
は、GoランタイムがOSから仮想メモリ空間を予約するために使用する低レベルの関数です。この関数は、指定されたアドレスから指定されたサイズの仮想メモリ領域を予約しようとします。ただし、この関数が返すアドレスは、OSのページサイズにはアラインされますが、Goランタイムが内部的に定義するPageSize
(OSのページサイズよりも大きい場合がある)にはアラインされない可能性があります。これは、OSがGoランタイムのPageSize
の概念を知らないためです。
4. Goのメモリ管理 (mheap
, MSpan
, MCentral
)
Goのランタイムは、独自のメモリ管理システムを持っています。
- ヒープ (Heap): Goプログラムが動的にメモリを割り当てる領域です。
mheap
: Goランタイムのグローバルなヒープ構造体で、メモリの割り当てと解放を管理します。MSpan
: 連続したメモリページ(GoランタイムのPageSize
単位)の集合を表す構造体です。mheap
はMSpan
のリストを管理し、異なるサイズのオブジェクトを効率的に割り当てられるようにします。MCentral
: 特定のサイズのオブジェクトを割り当てるためのキャッシュです。
このコミットは、mheap
が管理するヒープ領域の初期予約時に、その開始アドレスがGoランタイムのPageSize
に適切にアラインされるようにするための変更です。
技術的詳細
このコミットの技術的な核心は、runtime·SysReserve
が返すメモリ領域の開始アドレスが、Goランタイムが期待するPageSize
にアラインされていない可能性があるという問題に対処することです。
変更前は、runtime·SysReserve
が返すアドレスがPageSize
にアラインされていることを暗黙的に期待していました。しかし、OSのページサイズとGoランタイムのPageSize
が異なる場合、特にGoランタイムのPageSize
がOSのページサイズよりも大きい場合、SysReserve
が返すアドレスはOSのページサイズにはアラインされても、GoランタイムのPageSize
にはアラインされない可能性があります。
この問題を解決するために、以下の2つの主要な変更が行われました。
-
予約サイズの増加:
runtime·SysReserve
を呼び出す際に、要求するメモリサイズにPageSize
を追加で加算します。- 変更前:
runtime·SysReserve(p, bitmap_size + spans_size + arena_size)
- 変更後:
runtime·SysReserve(p, bitmap_size + spans_size + arena_size + PageSize)
これにより、予約されるメモリ領域は、必要なサイズに加えて、最大でPageSize - 1
バイトの「余分な」領域を持つことになります。
- 変更前:
-
予約アドレスの
PageSize
アラインメント:runtime·SysReserve
が返したアドレスを、PageSize
の境界に切り上げます。- 変更前は、アラインメントチェックとエラーハンドリングが行われていましたが、直接的なアラインメント調整は行われていませんでした。
- 変更後:
p = (byte*)ROUND((uintptr)p, PageSize);
ROUND
マクロ(または関数)は、指定されたアドレスを次のPageSize
の倍数に切り上げる役割を果たします。SysReserve
が返したアドレスがPageSize
にアラインされていなくても、追加で予約したPageSize
の範囲内で、PageSize
にアラインされた開始アドレスを見つけることができます。
この2つの変更を組み合わせることで、runtime·SysReserve
がどのようなアドレスを返しても、Goランタイムのヒープの開始アドレスが常にPageSize
にアラインされることが保証されます。これにより、将来的にGoランタイムがより大きなページサイズをサポートする際にも、メモリ管理システムが安定して動作するようになります。
また、変更前には存在したアラインメントチェックのruntime·printf
やruntime·throw
の呼び出しが削除されています。これは、新しいロジックによってアラインメントが保証されるため、これらのチェックが不要になったことを示しています。
コアとなるコードの変更箇所
src/pkg/runtime/malloc.goc
ファイル内の runtime·mallocinit
関数が変更されています。
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -427,7 +427,6 @@ runtime·mallocinit(void)
byte *p;
uintptr arena_size, bitmap_size, spans_size;
extern byte end[];
- byte *want;
uintptr limit;
uint64 i;
@@ -486,7 +485,7 @@ runtime·mallocinit(void)
spans_size = ROUND(spans_size, PageSize);
for(i = 0; i <= 0x7f; i++) {
p = (void*)(i<<40 | 0x00c0ULL<<32);
- p = runtime·SysReserve(p, bitmap_size + spans_size + arena_size);
+ p = runtime·SysReserve(p, bitmap_size + spans_size + arena_size + PageSize);
if(p != nil)
break;
}
@@ -528,16 +527,16 @@ runtime·mallocinit(void)
// So adjust it upward a little bit ourselves: 1/4 MB to get
// away from the running binary image and then round up
// to a MB boundary.
- want = (byte*)ROUND((uintptr)end + (1<<18), 1<<20);
- p = runtime·SysReserve(want, bitmap_size + spans_size + arena_size);
+ p = (byte*)ROUND((uintptr)end + (1<<18), 1<<20);
+ p = runtime·SysReserve(p, bitmap_size + spans_size + arena_size + PageSize);
if(p == nil)
runtime·throw(\"runtime: cannot reserve arena virtual address space\");
- if((uintptr)p & (((uintptr)1<<PageShift)-1))\n-\t\t\truntime·printf(\"runtime: SysReserve returned unaligned address %p; asked for %p\", p,\n-\t\t\t\tbitmap_size+spans_size+arena_size);\n }
- if((uintptr)p & (((uintptr)1<<PageShift)-1))\n-\t\truntime·throw(\"runtime: SysReserve returned unaligned address\");
+\n+\t// PageSize can be larger than OS definition of page size,\n+\t// so SysReserve can give us a PageSize-unaligned pointer.\n+\t// To overcome this we ask for PageSize more and round up the pointer.\n+\tp = (byte*)ROUND((uintptr)p, PageSize);\n
runtime·mheap.spans = (MSpan**)p;
runtime·mheap.bitmap = p + spans_size;
コアとなるコードの解説
-
byte *want;
の削除:- 変更前は、
want
という一時変数が使用されていましたが、変更後は不要になったため削除されました。これはコードの簡素化に貢献しています。
- 変更前は、
-
runtime·SysReserve
の呼び出し変更 (2箇所):- 変更前:
p = runtime·SysReserve(p, bitmap_size + spans_size + arena_size);
- 変更後:
p = runtime·SysReserve(p, bitmap_size + spans_size + arena_size + PageSize);
- この変更により、
SysReserve
に渡す予約サイズがPageSize
分だけ増加しました。これは、SysReserve
が返すアドレスがPageSize
にアラインされていない場合に備え、後でアラインメント調整を行うための「余裕」を持たせるためです。
- 変更前:
-
アラインメントチェックとエラーハンドリングの削除:
- 変更前には、
SysReserve
が返したアドレスがPageSize
にアラインされているかをチェックし、アラインされていない場合はruntime·printf
で警告を出したり、runtime·throw
でパニックを起こしたりするコードがありました。 - これらの行は削除されました。これは、次の変更で説明する
ROUND
処理によって、アドレスのアラインメントが保証されるため、これらのチェックが不要になったためです。
- 変更前には、
-
p = (byte*)ROUND((uintptr)p, PageSize);
の追加:- この行が新たに追加されました。
SysReserve
が返したポインタp
を、PageSize
の境界に切り上げています。ROUND
マクロ(または関数)は、p
のアドレスをPageSize
の最も近い倍数に切り上げる役割を果たします。例えば、PageSize
が8KBで、SysReserve
が8KBの倍数ではないアドレス(例: 0x1001000)を返した場合でも、このROUND
処理によって、次の8KB境界(例: 0x1002000)に調整されます。- この処理により、Goランタイムのヒープの開始アドレスが常に
PageSize
にアラインされることが保証されます。
これらの変更により、Goランタイムは、OSのページサイズとGoランタイムのPageSize
が異なる環境や、将来的にGoランタイムのPageSize
が変更される場合でも、安定したメモリ管理を維持できるようになります。
関連リンク
- Go言語のメモリ管理に関する公式ドキュメントやブログ記事
- Goランタイムのソースコードリポジトリ
- Goのガベージコレクションに関する資料