[インデックス 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.