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

[インデックス 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単位)の集合を表す構造体です。mheapMSpanのリストを管理し、異なるサイズのオブジェクトを効率的に割り当てられるようにします。
  • MCentral: 特定のサイズのオブジェクトを割り当てるためのキャッシュです。

このコミットは、mheapが管理するヒープ領域の初期予約時に、その開始アドレスがGoランタイムのPageSizeに適切にアラインされるようにするための変更です。

技術的詳細

このコミットの技術的な核心は、runtime·SysReserveが返すメモリ領域の開始アドレスが、Goランタイムが期待するPageSizeにアラインされていない可能性があるという問題に対処することです。

変更前は、runtime·SysReserveが返すアドレスがPageSizeにアラインされていることを暗黙的に期待していました。しかし、OSのページサイズとGoランタイムのPageSizeが異なる場合、特にGoランタイムのPageSizeがOSのページサイズよりも大きい場合、SysReserveが返すアドレスはOSのページサイズにはアラインされても、GoランタイムのPageSizeにはアラインされない可能性があります。

この問題を解決するために、以下の2つの主要な変更が行われました。

  1. 予約サイズの増加: runtime·SysReserveを呼び出す際に、要求するメモリサイズにPageSizeを追加で加算します。

    • 変更前: runtime·SysReserve(p, bitmap_size + spans_size + arena_size)
    • 変更後: runtime·SysReserve(p, bitmap_size + spans_size + arena_size + PageSize) これにより、予約されるメモリ領域は、必要なサイズに加えて、最大でPageSize - 1バイトの「余分な」領域を持つことになります。
  2. 予約アドレスのPageSizeアラインメント: runtime·SysReserveが返したアドレスを、PageSizeの境界に切り上げます。

    • 変更前は、アラインメントチェックとエラーハンドリングが行われていましたが、直接的なアラインメント調整は行われていませんでした。
    • 変更後: p = (byte*)ROUND((uintptr)p, PageSize); ROUNDマクロ(または関数)は、指定されたアドレスを次のPageSizeの倍数に切り上げる役割を果たします。SysReserveが返したアドレスがPageSizeにアラインされていなくても、追加で予約したPageSizeの範囲内で、PageSizeにアラインされた開始アドレスを見つけることができます。

この2つの変更を組み合わせることで、runtime·SysReserveがどのようなアドレスを返しても、Goランタイムのヒープの開始アドレスが常にPageSizeにアラインされることが保証されます。これにより、将来的にGoランタイムがより大きなページサイズをサポートする際にも、メモリ管理システムが安定して動作するようになります。

また、変更前には存在したアラインメントチェックのruntime·printfruntime·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;

コアとなるコードの解説

  1. byte *want; の削除:

    • 変更前は、wantという一時変数が使用されていましたが、変更後は不要になったため削除されました。これはコードの簡素化に貢献しています。
  2. 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にアラインされていない場合に備え、後でアラインメント調整を行うための「余裕」を持たせるためです。
  3. アラインメントチェックとエラーハンドリングの削除:

    • 変更前には、SysReserveが返したアドレスがPageSizeにアラインされているかをチェックし、アラインされていない場合はruntime·printfで警告を出したり、runtime·throwでパニックを起こしたりするコードがありました。
    • これらの行は削除されました。これは、次の変更で説明するROUND処理によって、アドレスのアラインメントが保証されるため、これらのチェックが不要になったためです。
  4. 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のガベージコレクションに関する資料

参考にした情報源リンク