[インデックス 14394] ファイルの概要
このコミットは、Goランタイムのメモリ管理に関する重要な変更を含んでいます。具体的には、64ビットシステムにおけるGoプログラムが確保できる最大メモリ量を、従来の16GBから128GBに拡張するものです。この変更は、大規模なデータセットを扱うアプリケーションや、より多くのメモリを必要とするワークロードに対応するために行われました。
コミット
commit 9799a5a4fd6ec85c52c48e73cb197006ca06c32e
Author: Russ Cox <rsc@golang.org>
Date: Tue Nov 13 12:45:08 2012 -0500
runtime: allow up to 128 GB of allocated memory
Incorporates code from CL 6828055.
Fixes #2142.
R=golang-dev, iant, devon.odell
CC=golang-dev
https://golang.org/cl/6826088
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9799a5a4fd6ec85c52c48e73cb197006ca06c32e
元コミット内容
runtime: allow up to 128 GB of allocated memory
Incorporates code from CL 6828055.
Fixes #2142.
R=golang-dev, iant, devon.odell
CC=golang-dev
https://golang.org/cl/6826088
変更の背景
この変更の主な背景は、Goプログラムが利用できるメモリ量の上限が16GBに制限されていたことです。当時のGoランタイムは、64ビットシステム上でもヒープ領域の最大サイズを16GBに設定していました。しかし、より大規模なアプリケーションやデータ処理のニーズが高まるにつれて、この16GBという制限がボトルネックとなるケースが増えてきました。
特に、Go issue #2142("runtime: 16GB memory limit on 64-bit systems")で報告されたように、ユーザーはGoプログラムがシステムに搭載されている物理メモリを十分に活用できないという問題に直面していました。この制限は、Goのメモリ管理メカニズム、特にヒープのビットマップ管理と仮想メモリの予約方法に起因していました。
このコミットは、この16GBの制限を緩和し、より多くのメモリをGoプログラムが利用できるようにすることで、Goの適用範囲を広げ、より大規模なシステムでの利用を可能にすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムのメモリ管理に関する基本的な概念を理解しておく必要があります。
- ヒープ (Heap): Goプログラムが動的にメモリを確保する領域です。
make
やnew
などで確保されるオブジェクトはヒープに配置されます。 - アリーナ (Arena): Goランタイムは、仮想メモリ空間の一部を「アリーナ」として予約します。これは、Goのヒープが使用できる連続した仮想アドレス空間の範囲を指します。このアリーナは、実際の物理メモリが割り当てられる前に、OSに対して予約されます。
- ビットマップ (Bitmap): Goのガベージコレクタ(GC)は、ヒープ上のどのメモリがポインタを含んでいるか、どのメモリがオブジェクトの開始点であるかなどを追跡するためにビットマップを使用します。このビットマップは、ヒープ領域の各ワード(またはバイト)に対応するビットを持ち、メモリのレイアウトに関する重要な情報を提供します。ビットマップのサイズは、管理するヒープのサイズに比例します。
runtime·SysReserve
: GoランタイムがOSに対して仮想メモリ空間を予約するために使用する内部関数です。これは、物理メモリの割り当てを伴わない「予約」であり、実際にメモリが使用される際にOSがページをコミットします。- 保守的ガベージコレクション (Conservative Garbage Collection): Goの初期のGCは、一部で保守的なアプローチを採用していました。これは、メモリ上の値がポインタであるかどうかを厳密に判断できない場合でも、それがポインタである可能性があれば、そのメモリを「生きている」とみなして回収しないというものです。このアプローチは、デバッグのしやすさや、特定のビットパターンが偶然メモリアドレスと一致してしまうことによる誤った回収を防ぐために、特定の仮想アドレス範囲を避けるという設計上の考慮事項につながります。
PageShift
: システムのメモリページサイズを2のべき乗で表した指数です。例えば、4KBのページサイズの場合、2^12 = 4096
なので、PageShift
は12になります。メモリ管理において、アドレスをページ単位で扱う際に使用されます。MHeapMap_Bits
: ヒープマップの計算に使用されるビット数です。これは、ヒープ全体をカバーするために必要なアドレスビット数を決定します。
技術的詳細
このコミットの技術的な核心は、Goランタイムが64ビットシステム上でヒープを管理する方法の根本的な変更にあります。
-
最大メモリ量 (
MaxMem
) の拡張:- 以前は、64ビットシステムでの
MaxMem
は16ULL<<30
、つまり16GBに固定されていました。 - 変更後、
MaxMem
は1ULL<<(MHeapMap_Bits+PageShift)
と定義されるようになりました。これは、MHeapMap_Bits
とPageShift
の合計ビット数で表現できる最大メモリ量を示します。 MHeapMap_Bits
が22から37 - PageShift
(通常は25)に増加したことで、MaxMem
は1ULL<<(25+12)
=1ULL<<37
= 128GBに拡張されました。
- 以前は、64ビットシステムでの
-
アリーナサイズ (
arena_size
) の変更:- ヒープのアリーナサイズも16GBから
MaxMem
(128GB)に変更されました。これにより、Goランタイムはより大きな仮想メモリ領域をヒープとして利用できるようになります。
- ヒープのアリーナサイズも16GBから
-
ビットマップサイズ (
bitmap_size
) の調整:- ヒープサイズが大きくなったことに伴い、ヒープのメタデータを管理するビットマップのサイズも調整されました。ビットマップは、ヒープの各ワード(
sizeof(void*)
)に対して4ビットを使用するように設計されており、arena_size / (sizeof(void*)*8/4)
という計算式でサイズが決定されます。ヒープが大きくなれば、ビットマップも大きくなります。
- ヒープサイズが大きくなったことに伴い、ヒープのメタデータを管理するビットマップのサイズも調整されました。ビットマップは、ヒープの各ワード(
-
仮想メモリ予約アドレスの変更:
runtime·SysReserve
関数を呼び出す際に、OSに推奨する仮想メモリの開始アドレスが0x00f8ULL<<32
から0x00c0ULL<<32
に変更されました。- このアドレス選択は、Goの保守的GCの特性と関連しています。特定のバイトパターン(特にUTF-8で無効なバイトシーケンス)を仮想アドレスに含めることで、偶然メモリ上のデータがポインタと誤認識されるリスクを低減しようとしています。
0x00f8
や0x00c0
のようなアドレスは、UTF-8の有効なシーケンスに含まれないバイト(f8
,f9
,fa
,fb
やc0
,c1
, ...,df
)を先頭に持つように選ばれています。これにより、GCが非ポインタデータを誤ってポインタと判断し、回収を妨げる可能性を低減します。- コメントには、以前の
0x11f8
の試みがOS Xでのスレッド割り当て時にメモリ不足エラーを引き起こしたことが記されており、このアドレス選択がプラットフォーム固有の問題を考慮したものであることが示唆されています。
-
MHeapMap_Bits
の計算方法の変更:MHeapMap_Bits
は、ヒープマップがカバーするアドレス空間のビット数を表します。以前は64ビットシステムで22ビットと固定されていましたが、これは16GBのヒープに対応していました(2^22 * 4KBページ = 16GB
)。- 新しい計算式
37 - PageShift
は、ヒープの最大サイズ(128GB、つまり37ビットのアドレス空間)からページシフト分を引くことで、ヒープマップが管理するアドレス範囲を正確に反映するように変更されました。これにより、ヒープマップが128GBのヒープ全体を適切にカバーできるようになります。
これらの変更により、Goランタイムは64ビットシステム上でより広大な仮想メモリ空間を効率的に管理し、大規模なアプリケーションのニーズに応えることができるようになりました。
コアとなるコードの変更箇所
src/pkg/runtime/malloc.goc
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -323,32 +323,30 @@ runtime·mallocinit(void)
// enough to hold 4 bits per allocated word.
if(sizeof(void*) == 8 && (limit == 0 || limit > (1<<30))) {
// On a 64-bit machine, allocate from a single contiguous reservation.
- // 16 GB should be big enough for now.
+ // 128 GB (MaxMem) should be big enough for now.
//
// The code will work with the reservation at any address, but ask
- // SysReserve to use 0x000000f800000000 if possible.
- // Allocating a 16 GB region takes away 36 bits, and the amd64
+ // SysReserve to use 0x000000c000000000 if possible.
+ // Allocating a 128 GB region takes away 37 bits, and the amd64
// doesn't let us choose the top 17 bits, so that leaves the 11 bits
- // in the middle of 0x00f8 for us to choose. Choosing 0x00f8 means
- // that the valid memory addresses will begin 0x00f8, 0x00f9, 0x00fa, 0x00fb.
- // None of the bytes f8 f9 fa fb can appear in valid UTF-8, and
- // they are otherwise as far from ff (likely a common byte) as possible.
- // Choosing 0x00 for the leading 6 bits was more arbitrary, but it
- // is not a common ASCII code point either. Using 0x11f8 instead
+ // in the middle of 0x00c0 for us to choose. Choosing 0x00c0 means
+ // that the valid memory addresses will begin 0x00c0, 0x00c1, ..., 0x0x00df.
+ // In little-endian, that's c0 00, c1 00, ..., df 00. None of those are valid
+ // UTF-8 sequences, and they are otherwise as far away from
+ // ff (likely a common byte) as possible. An earlier attempt to use 0x11f8
// caused out of memory errors on OS X during thread allocations.
// These choices are both for debuggability and to reduce the
// odds of the conservative garbage collector not collecting memory
// because some non-pointer block of memory had a bit pattern
// that matched a memory address.
//
- // Actually we reserve 17 GB (because the bitmap ends up being 1 GB)
- // but it hardly matters: fc is not valid UTF-8 either, and we have to
- // allocate 15 GB before we get that far.
+ // Actually we reserve 136 GB (because the bitmap ends up being 8 GB)
+ // but it hardly matters: e0 00 is not valid UTF-8 either.
//
// If this fails we fall back to the 32 bit memory mechanism
-\t\tarena_size = 16LL<<30;\n+\t\tarena_size = MaxMem;\n \t\tbitmap_size = arena_size / (sizeof(void*)*8/4);\n-\t\tp = runtime·SysReserve((void*)(0x00f8ULL<<32), bitmap_size + arena_size);\n+\t\tp = runtime·SysReserve((void*)(0x00c0ULL<<32), bitmap_size + arena_size);\n \t}\n \tif (p == nil) {\n \t\t// On a 32-bit machine, we can\'t typically get away
src/pkg/runtime/malloc.h
--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -114,12 +114,12 @@ enum
HeapAllocChunk = 1<<20, // Chunk size for heap growth
// Number of bits in page to span calculations (4k pages).
- // On 64-bit, we limit the arena to 16G, so 22 bits suffices.
- // On 32-bit, we don't bother limiting anything: 20 bits for 4G.
+ // On 64-bit, we limit the arena to 128GB, or 37 bits.
+ // On 32-bit, we don't bother limiting anything, so we use the full 32-bit address.
#ifdef _64BIT
-\tMHeapMap_Bits = 22,\n+\tMHeapMap_Bits = 37 - PageShift,\n #else
-\tMHeapMap_Bits = 20,\n+\tMHeapMap_Bits = 32 - PageShift,\n #endif
// Max number of threads to run garbage collection.
@@ -133,7 +133,7 @@ enum
// This must be a #define instead of an enum because it
// is so large.
#ifdef _64BIT
-#define MaxMem (16ULL<<30) /* 16 GB */\n+#define MaxMem (1ULL<<(MHeapMap_Bits+PageShift)) /* 128 GB */\n #else
#define MaxMem ((uintptr)-1)
#endif
コアとなるコードの解説
src/pkg/runtime/malloc.goc
の変更点
arena_size
の変更:arena_size = 16LL<<30;
からarena_size = MaxMem;
へ変更されました。これにより、ヒープのアリーナサイズが固定の16GBから、malloc.h
で定義されるMaxMem
(128GB)に動的に設定されるようになりました。これは、Goランタイムが利用できる仮想メモリ空間の最大値を直接的に引き上げる変更です。
runtime·SysReserve
の引数変更:p = runtime·SysReserve((void*)(0x00f8ULL<<32), bitmap_size + arena_size);
からp = runtime·SysReserve((void*)(0x00c0ULL<<32), bitmap_size + arena_size);
へ変更されました。- これは、OSに仮想メモリを予約する際に推奨するベースアドレスを
0x00f8ULL<<32
から0x00c0ULL<<32
に変更したものです。この変更は、前述の「保守的ガベージコレクション」の考慮事項に基づいています。新しいアドレス範囲も、UTF-8で無効なバイトパターンを含むように選ばれており、GCの誤認識を防ぐための工夫が凝らされています。
- コメントの更新:
- 最大メモリ量が16GBから128GBになったこと、および新しい予約アドレスに関する説明が更新されました。ビットマップのサイズが8GBになることや、
e0 00
が有効なUTF-8ではないことなども追記されています。
- 最大メモリ量が16GBから128GBになったこと、および新しい予約アドレスに関する説明が更新されました。ビットマップのサイズが8GBになることや、
src/pkg/runtime/malloc.h
の変更点
MHeapMap_Bits
の定義変更:- 64ビットシステムの場合、
MHeapMap_Bits = 22,
からMHeapMap_Bits = 37 - PageShift,
へ変更されました。 PageShift
は通常12(4KBページの場合)なので、37 - 12 = 25
となります。これにより、ヒープマップがカバーするアドレス空間のビット数が22ビットから25ビットに増加し、128GBのヒープを適切に管理できるようになります。- 32ビットシステムの場合も、
MHeapMap_Bits = 20,
からMHeapMap_Bits = 32 - PageShift,
へ変更されました。これは、32ビットアドレス空間全体をカバーするように調整されたことを意味します。
- 64ビットシステムの場合、
MaxMem
マクロの定義変更:- 64ビットシステムの場合、
#define MaxMem (16ULL<<30) /* 16 GB */
から#define MaxMem (1ULL<<(MHeapMap_Bits+PageShift)) /* 128 GB */
へ変更されました。 - この変更により、
MaxMem
はMHeapMap_Bits
とPageShift
の値に基づいて動的に計算されるようになり、ヒープの最大サイズが128GBに拡張されたことを明確に示しています。
- 64ビットシステムの場合、
これらの変更は、Goランタイムのメモリ管理の根幹に関わるものであり、Goプログラムがより大規模なメモリを効率的に利用するための基盤を構築しています。
関連リンク
- Go issue #2142: https://github.com/golang/go/issues/2142
- Go Change List 6826088: https://golang.org/cl/6826088
参考にした情報源リンク
- Go issue #2142の議論
- Goランタイムのメモリ管理に関するドキュメントやブログ記事 (一般的なGoのメモリ管理の概念を理解するために参照)
- UTF-8のエンコーディングに関する情報 (特定のバイトパターンがなぜ避けられるのかを理解するため)
- 仮想メモリと
SysReserve
に関する一般的なOSの知識