[インデックス 16544] ファイルの概要
このコミットは、Goランタイムのメモリ管理において、spans_size(Goのヒープ管理におけるスパンメタデータのサイズ)をページ境界に切り上げる変更を導入しています。これは、システムが非ページアラインなメモリ制限を持つ場合に、メモリ割り当ての堅牢性を向上させることを目的としています。
コミット
commit ccd1d07cc44f3ca033ab7ad9e93ebf97ff3fa94c
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Wed Jun 12 05:22:49 2013 +0800
runtime: round spans_size up to page boundary
in case we have weird (not page aligned) memory limit.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/10199043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ccd1d07cc44f3ca033ab7ad9e93ebf97ff3fa94c
元コミット内容
runtime: round spans_size up to page boundary
in case we have weird (not page aligned) memory limit.
変更の背景
Goランタイムのメモリ管理は、mheapと呼ばれるグローバルヒープ、mcentral、mcacheといったコンポーネントによって行われます。これらのコンポーネントは、メモリをspan(mspan構造体で表現される連続したメモリページブロック)という単位で管理します。spans_sizeは、これらのmspan構造体へのポインタを格納するための領域のサイズを指します。
このコミットが導入された背景には、Goランタイムがメモリを予約する際に、オペレーティングシステム(OS)から提供されるメモリ制限が、Goランタイムが内部的に使用するページサイズ(PageSize)の境界にアラインされていない(整列されていない)場合に問題が発生する可能性がありました。
具体的には、runtime·mallocinit関数は、Goプログラムが起動する際にメモリ管理システムを初期化します。この初期化プロセス中に、spans_sizeを含む様々なメモリ領域のサイズが計算され、OSに対してメモリの予約(runtime·SysReserve)が行われます。もしspans_sizeがページ境界に正確にアラインされていない場合、OSがメモリを割り当てる際に、要求されたサイズと実際に割り当てられるサイズとの間に不整合が生じ、予期せぬメモリ管理上の問題やクラッシュを引き起こす可能性がありました。
この変更は、特に「奇妙な(ページアラインされていない)メモリ制限」がある場合に、spans_sizeが常にGoランタイムのページ境界に切り上げられるようにすることで、このような潜在的な問題を回避し、メモリ管理の堅牢性と安定性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、Goランタイムのメモリ管理に関するいくつかの基本的な概念を理解しておく必要があります。
- Goランタイムのメモリページ (
PageSize): Goランタイムは、独自のメモリ管理のために8KB(8192バイト)のメモリページを使用します。これはOSのページサイズとは異なる場合がありますが、通常はOSのページサイズの整数倍になります。PageSizeは、Goランタイムがメモリを割り当てる際の基本的な単位です。 PageShift:PageShiftは、PageSizeを2の累乗で表現するためのシフト量です。PageSize = 1 << PageShiftの関係にあります。Goランタイムでは、PageShiftは通常13であり、1 << 13 = 8192となります。この値は、ページ境界でのアラインメント計算に利用されます。- スパン (
mspan): スパンは、Goランタイムのヒープ上で連続したメモリページのブロックを指します。mspan構造体は、これらのスパンに関するメタデータ(例えば、スパンが管理するページの数、オブジェクトのサイズクラスなど)を保持します。Goのメモリ管理は、これらのスパンを単位としてメモリを割り当て、解放します。 spans_size: これは、Goランタイムのヒープ管理において、mspan構造体へのポインタを格納するために予約されるメモリ領域のサイズです。この領域は、Goヒープ全体のメモリマップのような役割を果たし、特定のアドレスがどのスパンに属するかを迅速に特定できるようにします。runtime·mallocinit: Goプログラムの起動時に呼び出される関数で、Goランタイムのメモリ管理システムを初期化します。この関数内で、ヒープのサイズ、ビットマップのサイズ、そしてspans_sizeなどが計算され、OSから必要なメモリが予約されます。runtime·SysReserve: GoランタイムがOSに対してメモリ領域を予約するために使用する内部関数です。この関数は、指定されたアドレス範囲でメモリを予約しようとしますが、OSは要求されたアドレスをヒントとして扱い、必ずしもその正確なアドレスに割り当てるとは限りません。
技術的詳細
このコミットの核心は、spans_sizeの計算結果をGoランタイムのページ境界に切り上げるという点にあります。
元のコードでは、spans_sizeはarena_size / PageSize * sizeof(runtime·mheap.spans[0])という式で計算されていました。ここで、arena_sizeはGoヒープ全体のサイズ、PageSizeはGoランタイムのメモリページサイズ、sizeof(runtime·mheap.spans[0])はmspan構造体へのポインタのサイズです。この計算結果は、必ずしもPageSizeの倍数になるとは限りません。
OSがメモリを割り当てる際、通常はページ単位で行われます。もしspans_sizeがページ境界にアラインされていない場合、runtime·SysReserveが要求するメモリサイズがOSのページアラインメントと合致せず、OSが実際に割り当てるメモリ領域が要求よりも大きくなったり、予期せぬアドレスに割り当てられたりする可能性があります。これは、Goランタイムが後続のメモリ管理でその領域を使用する際に、アドレス計算の不整合やメモリ保護違反などの問題を引き起こす原因となり得ます。
この変更では、以下のビット演算を使用してspans_sizeをPageSizeの倍数に切り上げています。
spans_size = (spans_size + ((1<<PageShift) - 1)) & ~((1<<PageShift) - 1);
この式は、一般的な「Nの倍数に切り上げる」ためのビット演算テクニックです。
1 << PageShiftはPageSizeと同じ値(8192)です。(1 << PageShift) - 1はPageSize - 1と同じ値(8191)です。これは、PageSizeの倍数ではない部分を抽出するためのマスクとして機能します。例えば、PageSizeが8192(2^13)の場合、PageSize - 1は0b1111111111111(13個の1)となります。spans_size + ((1<<PageShift) - 1): これにより、spans_sizeがPageSizeの倍数でない場合でも、次のPageSizeの倍数に「到達」するように値を増やします。例えば、spans_sizeが8193の場合、8193 + 8191 = 16384となります。~((1<<PageShift) - 1): これは、PageSizeの倍数ではない部分をゼロにするためのマスクです。例えば、PageSize - 1が0b1111111111111の場合、そのビット反転は0b...0000000000000(下位13ビットが0)となります。&: 論理AND演算により、spans_size + ((1<<PageShift) - 1)の結果から、PageSizeの倍数ではない下位ビットを強制的にゼロにします。これにより、結果は常にPageSizeの倍数になります。
この操作により、spans_sizeは常にGoランタイムのページ境界にアラインされるようになり、runtime·SysReserveがOSにメモリを要求する際に、より予測可能で堅牢な動作が保証されます。これにより、特にOSが非標準的なメモリ制限を課す環境下でのGoプログラムの安定性が向上します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/malloc.gocファイルにあります。
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -351,6 +351,8 @@ runtime·mallocinit(void)
arena_size = MaxMem;
bitmap_size = arena_size / (sizeof(void*)*8/4);\n spans_size = arena_size / PageSize * sizeof(runtime·mheap.spans[0]);
+\t\t// round spans_size to pages
+\t\tspans_size = (spans_size + ((1<<PageShift) - 1)) & ~((1<<PageShift) - 1);
p = runtime·SysReserve((void*)(0x00c0ULL<<32), bitmap_size + spans_size + arena_size);
}
if (p == nil) {
@@ -379,6 +381,8 @@ runtime·mallocinit(void)\n arena_size = bitmap_size * 8;
spans_size = arena_size / PageSize * sizeof(runtime·mheap.spans[0]);
}\n+\t\t// round spans_size to pages
+\t\tspans_size = (spans_size + ((1<<PageShift) - 1)) & ~((1<<PageShift) - 1);
コアとなるコードの解説
上記のdiffが示すように、runtime·mallocinit関数内の2箇所でspans_sizeの計算後に、ページ境界への切り上げ処理が追加されています。
-
最初の追加箇所: これは、
MaxMem(システムが利用可能な最大メモリ量)に基づいてarena_sizeが設定される初期化パスです。ここでspans_sizeが計算された後、直ちにページ境界への切り上げが行われます。// round spans_size to pages spans_size = (spans_size + ((1<<PageShift) - 1)) & ~((1<<PageShift) - 1); -
二番目の追加箇所: これは、
MaxMemが設定されていない場合や、他の条件に基づいてarena_sizeが再計算される別の初期化パスです。ここでも同様に、spans_sizeが再計算された後、ページ境界への切り上げが適用されます。// round spans_size to pages spans_size = (spans_size + ((1<<PageShift) - 1)) & ~((1<<PageShift) - 1);
この変更により、spans_sizeが常にGoランタイムのページサイズ(PageSize)の倍数になることが保証されます。これにより、runtime·SysReserveがOSにメモリを要求する際に、要求されるサイズが常にページアラインされ、OSとのメモリ割り当ての整合性が保たれます。結果として、Goランタイムのメモリ管理がより堅牢になり、特に非標準的なメモリ構成を持つシステムでの安定性が向上します。
関連リンク
- Go CL (Change List): https://golang.org/cl/10199043
参考にした情報源リンク
- Go runtime memory management concepts:
- https://go101.org/article/memory-management.html
- https://sobyte.net/post/2022-03/go-memory-management/
- https://andrestc.com/post/go-memory-management/
- https://dev.to/aickin/go-memory-management-an-in-depth-look-310
- https://deepu.tech/go-memory-management/
- https://povilasv.me/go-memory-management/
- https://medium.com/@ankur_anand/go-memory-management-a-deep-dive-into-the-go-runtime-memory-allocator-1c2d4b2d4b2d
- Go runtime source code (PageShift definition):
- https://github.com/golang/go/blob/master/src/runtime/malloc.go (Note: The exact file path might vary slightly in older versions, but the concept remains.)
- Bitwise operations for rounding up: General programming knowledge.