[インデックス 18940] ファイルの概要
このコミットは、Goランタイムのメモリ管理における重要な改善を導入しています。具体的には、ヒープメモリがオペレーティングシステムによって実際に予約されているかどうかを正確に記録するための変更です。これまでの実装では、メモリが実際に予約されたかどうかの明確な概念がなく、32ビットモードか64ビットモードか、および(GNU/Linuxの場合)要求されたアドレスに基づいてチェックを行っていましたが、要求されたアドレスと返されたアドレスを混同していました。この変更により、メモリ予約のステータスがより正確に追跡され、ランタイムのメモリ管理の堅牢性が向上します。
コミット
commit 4ebfa8319914e1ed9727592d1fa360ce339b7597
Author: Ian Lance Taylor <iant@golang.org>
Date: Tue Mar 25 13:22:19 2014 -0700
runtime: accurately record whether heap memory is reserved
The existing code did not have a clear notion of whether
memory has been actually reserved. It checked based on
whether in 32-bit mode or 64-bit mode and (on GNU/Linux) the
requested address, but it confused the requested address and
the returned address.
LGTM=rsc
R=rsc, dvyukov
CC=golang-codereviews, michael.hudson
https://golang.org/cl/79610043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4ebfa8319914e1ed9727592d1fa360ce339b7597
元コミット内容
このコミットの元の内容は、Goランタイムがヒープメモリの予約状況を正確に記録するように修正することです。既存のコードは、メモリが実際に予約されたかどうかの明確な概念を持っておらず、32ビットモードか64ビットモードか、および(GNU/Linuxの場合)要求されたアドレスに基づいてチェックを行っていましたが、要求されたアドレスと返されたアドレスを混同していました。
変更の背景
Goランタイムは、プログラムが使用するメモリを効率的に管理するために、オペレーティングシステム(OS)から仮想メモリを要求します。このメモリ管理には、大きく分けて「予約 (reserve)」と「コミット (commit)」という2つの段階があります。
- 予約 (Reserve): これは、特定の仮想アドレス範囲を将来使用するために確保するプロセスです。この段階では、物理メモリは割り当てられず、単にそのアドレス範囲が他のプロセスに割り当てられないようにマークされるだけです。OSによっては、この予約が非常に「軽い」操作であり、実際に物理メモリが割り当てられるまで厳密なチェックを行わない場合があります。特に64ビットシステムでは、非常に広大な仮想アドレス空間が存在するため、OSは実際に必要になるまで物理メモリの割り当てを遅延させることが一般的です。
- コミット (Commit): これは、予約された仮想アドレス範囲に実際に物理メモリを割り当てるプロセスです。プログラムがそのメモリにアクセスしようとすると、OSはページフォールトを処理し、必要に応じて物理ページを割り当てます。
このコミット以前のGoランタイムでは、SysReserve
関数がメモリを予約する際に、そのメモリがOSによって実際に「予約された」のか、それとも単に「利用可能であることが確認された」だけなのかを正確に区別できていませんでした。特に64ビットシステムや、ulimit -v
のような仮想メモリ制限が設定されている環境では、SysReserve
が成功したと見せかけても、実際にはOSがそのアドレス空間を厳密に予約していないケースがありました。これにより、SysMap
(予約されたメモリをコミットする関数)が後で呼び出された際に、予期せぬ問題(例えば、メモリ不足エラー)が発生する可能性がありました。
また、既存のコードは、SysReserve
に渡された「要求されたアドレス」と、OSが実際に返した「割り当てられたアドレス」を混同していました。これは、OSが要求されたアドレスとは異なるアドレスにメモリを割り当てる可能性があるため、問題を引き起こす可能性がありました。
このコミットは、これらの問題を解決し、GoランタイムがOSからのメモリ予約のステータスをより正確に把握できるようにすることを目的としています。これにより、メモリ管理の堅牢性が向上し、特に大規模なメモリを扱うアプリケーションや、様々なOS環境での安定性が確保されます。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
仮想メモリ (Virtual Memory):
- 現代のオペレーティングシステムが提供するメモリ管理の抽象化層です。各プロセスは、物理メモリの実際の配置とは独立した、連続した仮想アドレス空間を持っているかのように見えます。
- 仮想アドレスは、メモリ管理ユニット(MMU)によって物理アドレスに変換されます。
- これにより、プロセスは物理メモリのサイズよりも大きなメモリを使用できる(スワップ領域の利用)ようになり、複数のプロセスが互いに干渉することなくメモリを共有できるようになります。
-
メモリの予約 (Reservation) とコミット (Commitment):
- 予約 (Reserve): 仮想アドレス空間の一部を、将来使用するために確保することです。この段階では、対応する物理メモリは割り当てられません。OSは、そのアドレス範囲が他の目的で使用されないようにマークするだけです。Windowsの
VirtualAlloc
のMEM_RESERVE
フラグや、Unix系OSのmmap
でPROT_NONE
を指定する操作がこれに該当します。 - コミット (Commit): 予約された仮想アドレス空間に実際に物理メモリを割り当てることです。これにより、プログラムはそのメモリ領域にデータを読み書きできるようになります。Windowsの
VirtualAlloc
のMEM_COMMIT
フラグや、Unix系OSのmmap
でPROT_READ|PROT_WRITE
などの保護フラグを指定する操作がこれに該当します。 - OSによっては、予約操作が非常に「軽い」ものであり、実際に物理メモリがコミットされるまで、そのアドレス空間が本当に利用可能であるかどうかの厳密なチェックを行わない場合があります。特に64ビットシステムでは、広大な仮想アドレス空間を効率的に管理するために、このような「楽観的な」予約が行われることがあります。
- 予約 (Reserve): 仮想アドレス空間の一部を、将来使用するために確保することです。この段階では、対応する物理メモリは割り当てられません。OSは、そのアドレス範囲が他の目的で使用されないようにマークするだけです。Windowsの
-
mmap
システムコール (Unix-like systems):- Unix系OSでメモリマッピングを行うためのシステムコールです。ファイルや匿名メモリ領域をプロセスの仮想アドレス空間にマッピングするために使用されます。
mmap(addr, len, prot, flags, fd, offset)
addr
: マッピングを希望する開始アドレス。NULL
の場合、OSが適切なアドレスを選択します。len
: マッピングするバイト数。prot
: メモリ保護フラグ(例:PROT_NONE
(アクセス不可),PROT_READ
(読み取り),PROT_WRITE
(書き込み),PROT_EXEC
(実行))。flags
: マッピングのタイプや動作を制御するフラグ(例:MAP_ANON
(匿名メモリ),MAP_PRIVATE
(プライベートコピー),MAP_FIXED
(addr
を厳密に要求))。
PROT_NONE
とMAP_ANON
を組み合わせて使用することで、物理メモリを割り当てずに仮想アドレス空間を予約する目的で利用されます。
-
VirtualAlloc
API (Windows):- Windowsで仮想メモリを操作するためのAPIです。
VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
lpAddress
: 割り当てを希望する開始アドレス。NULL
の場合、システムが決定します。dwSize
: 割り当てる領域のサイズ。flAllocationType
: 割り当てのタイプ(例:MEM_RESERVE
(予約),MEM_COMMIT
(コミット))。flProtect
: 領域のメモリ保護(例:PAGE_READWRITE
)。
-
Goランタイムのメモリ管理 (MHeap, Arenas):
- Goランタイムは独自のヒープマネージャを持っています。これは、OSから大きな仮想メモリの塊(アリーナ、
arena
)を予約し、そのアリーナ内でオブジェクトを割り当てて管理します。 MHeap
構造体は、Goランタイムのグローバルなヒープの状態を管理します。これには、アリーナの開始アドレス、使用中のアドレス、終了アドレスなどが含まれます。SysReserve
とSysMap
は、GoランタイムがOSとやり取りして仮想メモリを予約・コミットするための低レベルな関数です。
- Goランタイムは独自のヒープマネージャを持っています。これは、OSから大きな仮想メモリの塊(アリーナ、
技術的詳細
このコミットの主要な技術的変更点は、Goランタイムのメモリ予約メカニズムに「実際に予約されたかどうか」を示すbool
型のフラグを導入したことです。
具体的には、以下の変更が行われました。
-
runtime·SysReserve
関数のシグネチャ変更:- 変更前:
void* runtime·SysReserve(void *v, uintptr nbytes)
- 変更後:
void* runtime·SysReserve(void *v, uintptr nbytes, bool *reserved)
- 新しい
bool *reserved
引数は、SysReserve
が呼び出し元に、実際にメモリがOSによって予約されたかどうか(true
)または単に利用可能であることが確認されただけか(false
)を伝えるためのポインタです。これにより、呼び出し元はOSの挙動に基づいて後続の処理を調整できるようになります。
- 変更前:
-
runtime·SysMap
関数のシグネチャ変更:- 変更前:
void runtime·SysMap(void *v, uintptr nbytes, uint64 *stat)
- 変更後:
void runtime·SysMap(void *v, uintptr nbytes, bool reserved, uint64 *stat)
- 新しい
bool reserved
引数は、SysMap
がコミットしようとしているメモリ領域が、以前のSysReserve
呼び出しによって実際に予約されたものなのかどうかを示します。SysMap
はこの情報を使用して、OS固有の挙動(特に64ビットシステムでの「楽観的な」予約)を適切に処理します。例えば、reserved
がfalse
の場合、SysMap
はmmap
を呼び出して、その領域を実際にコミットするだけでなく、必要であれば予約も行います。
- 変更前:
-
MHeap
構造体へのarena_reserved
フィールドの追加:src/pkg/runtime/malloc.h
のMHeap
構造体にbool arena_reserved;
フィールドが追加されました。- このフィールドは、GoランタイムのメインヒープアリーナがOSによって実際に予約されているかどうかを追跡します。
runtime·mallocinit
およびruntime·MHeap_SysAlloc
でSysReserve
が呼び出された際に、このフラグが設定されます。
-
OS固有のメモリ管理コードの更新:
src/pkg/runtime/mem_*.c
ファイル(Darwin, Dragonfly, FreeBSD, Linux, NaCl, NetBSD, OpenBSD, Plan9, Solaris, Windows)が、新しいSysReserve
とSysMap
のシグネチャに合わせて更新されました。- これらのファイルでは、各OSの
mmap
やVirtualAlloc
の挙動に基づいて、reserved
フラグが適切に設定されます。- 多くの64ビットUnix系OSでは、
SysReserve
が非常に大きなメモリ領域(例えば、4GB以上)を要求する場合、*reserved
をfalse
に設定し、単に要求されたアドレスを返すことで、実際の予約をSysMap
に委ねるようになりました。これは、ulimit -v
のような制限がある環境で、過剰な仮想メモリ予約によるエラーを避けるためです。 - WindowsやDarwin(macOS)など、
SysReserve
が常に実際の予約を行うOSでは、*reserved
は常にtrue
に設定されます。 - Plan9では、
SysReserve
がSysAlloc
を呼び出すため、常に*reserved = true
となります。
- 多くの64ビットUnix系OSでは、
-
ヒープ初期化とアリーナ拡張ロジックの更新:
src/pkg/runtime/malloc.goc
内のruntime·mallocinit
(ヒープ初期化)とruntime·MHeap_SysAlloc
(アリーナ拡張)関数が、SysReserve
の新しいシグネチャを使用し、arena_reserved
フラグを適切に設定・利用するように変更されました。- 特に
runtime·MHeap_SysAlloc
では、32ビットモードでのアリーナ拡張時に、arena_reserved
フラグを考慮してSysMap
を呼び出すようになりました。これにより、アリーナの一部が予約済みで、一部がそうでないという不整合な状態を防ぐことができます。
-
ビットマップとスパンのマップ処理の更新:
src/pkg/runtime/mgc0.c
のruntime·MHeap_MapBits
とsrc/pkg/runtime/mheap.c
のruntime·MHeap_MapSpans
が、SysMap
を呼び出す際にh->arena_reserved
フラグを渡すように変更されました。これにより、これらの内部データ構造がマップされる際にも、アリーナの予約状況が正確に考慮されます。
これらの変更により、GoランタイムはOSのメモリ予約の挙動をより正確に把握し、それに基づいてメモリ管理を最適化できるようになりました。特に、64ビットシステムでの大規模なヒープの管理において、より堅牢で効率的な動作が期待されます。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
-
src/pkg/runtime/malloc.goc
:runtime·mallocinit
関数とruntime·MHeap_SysAlloc
関数で、runtime·SysReserve
の呼び出しにbool *reserved
引数が追加され、その結果がMHeap
構造体の新しいフィールドarena_reserved
に格納されるようになりました。runtime·MHeap_SysAlloc
内でruntime·SysMap
を呼び出す際にも、h->arena_reserved
が引数として渡されるようになりました。
-
src/pkg/runtime/malloc.h
:runtime·SysReserve
とruntime·SysMap
関数のプロトタイプが変更され、新しいbool
引数が追加されました。MHeap
構造体にbool arena_reserved;
フィールドが追加されました。runtime·SysReserve
とruntime·SysMap
のコメントが更新され、新しいreserved
引数の意味と、OSによるメモリ予約の挙動に関する詳細が記述されました。
-
src/pkg/runtime/mem_*.c
(各OS固有のメモリ管理ファイル):mem_darwin.c
,mem_dragonfly.c
,mem_freebsd.c
,mem_linux.c
,mem_nacl.c
,mem_netbsd.c
,mem_openbsd.c
,mem_plan9.c
,mem_solaris.c
,mem_windows.c
- これらのファイル内の
runtime·SysReserve
とruntime·SysMap
の実装が、新しいシグネチャに合わせて変更されました。 runtime·SysReserve
の実装内で、OSの挙動に基づいて*reserved
引数にtrue
またはfalse
が設定されるようになりました。特に、64ビットシステムで大きなメモリ領域を予約する際に、OSが実際に予約を行わない可能性がある場合にfalse
を設定するロジックが追加されました。runtime·SysMap
の実装内で、reserved
引数の値に基づいて、メモリをコミットする際の挙動が調整されるようになりました。reserved
がfalse
の場合、mmap
を呼び出して実際にメモリをコミットする際に、必要に応じて予約も同時に行うような処理が追加されました。
-
src/pkg/runtime/mgc0.c
:runtime·MHeap_MapBits
関数で、runtime·SysMap
の呼び出しにh->arena_reserved
が引数として渡されるようになりました。
-
src/pkg/runtime/mheap.c
:runtime·MHeap_MapSpans
関数で、runtime·SysMap
の呼び出しにh->arena_reserved
が引数として渡されるようになりました。
これらの変更は、Goランタイムのメモリ管理の根幹に関わる部分であり、OSとのインタフェース層に新たな情報伝達メカニズムを導入することで、より正確で堅牢なメモリ管理を実現しています。
コアとなるコードの解説
src/pkg/runtime/malloc.h
// SysReserve reserves address space without allocating memory.
// If the pointer passed to it is non-nil, the caller wants the
// reservation there, but SysReserve can still choose another
// location if that one is unavailable. On some systems and in some
// cases SysReserve will simply check that the address space is
// available and not actually reserve it. If SysReserve returns
// non-nil, it sets *reserved to true if the address space is
// reserved, false if it has merely been checked.
// NOTE: SysReserve returns OS-aligned memory, but the heap allocator
// may use larger alignment, so the caller must be careful to realign the
// memory obtained by SysAlloc.
//
// SysMap maps previously reserved address space for use.
// The reserved argument is true if the address space was really
// reserved, not merely checked.
//
// SysFault marks a (already SysAlloc'd) region to fault
// if accessed. Used only for debugging the runtime.
void* runtime·SysReserve(void *v, uintptr nbytes, bool *reserved);
void runtime·SysMap(void *v, uintptr nbytes, bool reserved, uint64 *stat);
// ... (MHeap struct definition) ...
struct MHeap
{
// ...
byte *arena_start;
byte *arena_used;
byte *arena_end;
bool arena_reserved; // 新しく追加されたフィールド
// ...
};
SysReserve
とSysMap
の関数シグネチャが変更され、reserved
引数が追加されました。SysReserve
のコメントが更新され、*reserved
引数が「実際に予約されたか」または「単にチェックされただけか」を示すことが明記されました。SysMap
のコメントも更新され、reserved
引数が「実際に予約された領域か」を示すことが明記されました。MHeap
構造体にarena_reserved
という新しいbool
フィールドが追加されました。これは、GoランタイムのメインヒープアリーナがOSによって実際に予約されているかどうかを追跡します。
src/pkg/runtime/malloc.goc
void runtime·mallocinit(void) {
// ...
bool reserved; // 新しいローカル変数
// ...
reserved = false; // 初期化
// ...
// 64-bit buildの場合の初期アリーナ予約
p = runtime·SysReserve(p, p_size, &reserved); // SysReserveに&reservedを渡す
// ...
// 32-bit buildの場合の初期アリーナ予約
p = runtime·SysReserve(p, p_size, &reserved); // SysReserveに&reservedを渡す
// ...
runtime·mheap.arena_reserved = reserved; // MHeapに予約状態を保存
// ...
}
byte* runtime·MHeap_SysAlloc(MHeap *h, uintptr n) {
// ...
bool reserved; // 新しいローカル変数
// ...
if(n > h->arena_end - h->arena_used) {
// 32-bitモードでアリーナを拡張する場合
// ...
p = runtime·SysReserve(h->arena_end, p_size, &reserved); // SysReserveに&reservedを渡す
if(p == h->arena_end) {
h->arena_end = new_end;
h->arena_reserved = reserved; // MHeapに予約状態を保存
} else if(p+p_size <= h->arena_start + MaxArena32) {
// ...
h->arena_reserved = reserved; // MHeapに予約状態を保存
}
// ...
}
if(n <= h->arena_end - h->arena_used) {
// 既存の予約からメモリをマップする場合
p = h->arena_used;
runtime·SysMap(p, n, h->arena_reserved, &mstats.heap_sys); // SysMapにh->arena_reservedを渡す
h->arena_used += n;
// ...
}
// ...
}
runtime·mallocinit
とruntime·MHeap_SysAlloc
で、SysReserve
の呼び出しが更新され、reserved
変数のアドレスが渡されるようになりました。SysReserve
から返されたreserved
の値が、runtime·mheap.arena_reserved
に格納されるようになりました。これにより、ヒープアリーナが実際に予約されているかどうかの状態がグローバルに保持されます。runtime·MHeap_SysAlloc
内でSysMap
を呼び出す際に、h->arena_reserved
の値が引数として渡されるようになりました。これは、メモリをコミットする際に、その領域が実際に予約されているかどうかの情報に基づいてOS固有の処理を行うためです。
src/pkg/runtime/mem_linux.c
(Linux固有の例)
void* runtime·SysReserve(void *v, uintptr n, bool *reserved) {
void *p;
// On 64-bit, people with ulimit -v set complain if we reserve too
// much address space. Instead, assume that the reservation is okay
// if we can reserve at least 64K and check the assumption in SysMap.
// Only user-mode Linux (UML) rejects these requests.
if(sizeof(void*) == 8 && n > 1LL<<32) { // 64ビットで非常に大きな領域を予約する場合
// 64KBだけmmapしてみて、成功すれば残りはSysMapで処理する
p = mmap_fixed(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
if (p != v) {
if(p >= (void*)4096)
runtime·munmap(p, 64<<10);
return nil;
}
runtime·munmap(p, 64<<10);
*reserved = false; // 実際に予約されていないことを示す
return v; // 要求されたアドレスを返す
}
p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
if((uintptr)p < 4096)
return nil;
*reserved = true; // 実際に予約されたことを示す
return p;
}
void runtime·SysMap(void *v, uintptr n, bool reserved, uint64 *stat) {
void *p;
runtime·xadd64(stat, n);
// On 64-bit, we don't actually have v reserved, so tread carefully.
if(!reserved) { // 予約されていない場合
p = mmap_fixed(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0); // 予約とコミットを同時に行う
if(p == (void*)ENOMEM)
runtime·throw("runtime: out of memory");
if(p != v)
runtime·throw("runtime: SysMap: cannot map at address");
} else { // 予約されている場合
p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0); // コミットのみ
if(p == (void*)ENOMEM)
runtime·throw("runtime: out of memory");
if(p != v)
runtime·throw("runtime: SysMap: cannot map at address");
}
}
runtime·SysReserve
では、64ビットシステムで非常に大きなメモリ領域(4GB以上)を予約しようとする場合、OSが実際に予約を行わない可能性があるため、*reserved
をfalse
に設定し、要求されたアドレスをそのまま返します。これは、ulimit -v
のような制限がある環境での問題を回避するためです。それ以外の場合は、mmap
が成功すれば*reserved
をtrue
に設定します。runtime·SysMap
では、reserved
引数の値に基づいて挙動が変わります。!reserved
(予約されていない場合):mmap_fixed
を呼び出し、PROT_READ|PROT_WRITE
を指定して、予約とコミットを同時に行います。これは、SysReserve
が実際の予約を行わなかった場合に、SysMap
がその役割を果たすためです。reserved
(予約されている場合):mmap
を呼び出し、MAP_FIXED
フラグを使用して、既に予約されたアドレスに物理メモリをコミットします。
これらの変更により、GoランタイムはOSのメモリ予約の挙動をより正確に把握し、それに基づいてメモリ管理を最適化できるようになりました。特に、64ビットシステムでの大規模なヒープの管理において、より堅牢で効率的な動作が期待されます。
関連リンク
- Goのメモリ管理に関する公式ドキュメントやブログ記事(コミット当時のもの、または関連する概念を説明するもの)
- Goのメモリ管理の進化に関する記事: https://go.dev/blog/go1.14-memory-management (これはコミットより新しいですが、Goのメモリ管理の概念を理解するのに役立ちます)
- Goのガベージコレクションに関する記事: https://go.dev/blog/go15gc (これもコミットより新しいですが、メモリ管理の文脈で関連します)
mmap
システムコールに関するmanページやドキュメントVirtualAlloc
APIに関するMicrosoftのドキュメント
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commit/4ebfa8319914e1ed9727592d1fa360ce339b7597
- Goのコードレビューシステム (Gerrit): https://golang.org/cl/79610043
- 仮想メモリ、メモリ予約、コミットに関する一般的なOSのドキュメントや解説記事 (例: Wikipedia, OSの教科書など)
mmap
に関するLinux man page:man 2 mmap
VirtualAlloc
に関するMicrosoft Learn: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc- Goのソースコード (特に
src/pkg/runtime
ディレクトリ) - GoのIssue Tracker (関連するバグ報告や議論がある場合)I have generated the detailed explanation in Markdown format, following all the specified instructions and chapter structure. I have included background, prerequisite knowledge, technical details, and explanations of the core code changes. I have also included relevant links and references. The output is to standard output only, as requested.
# [インデックス 18940] ファイルの概要
このコミットは、Goランタイムのメモリ管理における重要な改善を導入しています。具体的には、ヒープメモリがオペレーティングシステムによって実際に予約されているかどうかを正確に記録するための変更です。これまでの実装では、メモリが実際に予約されたかどうかの明確な概念がなく、32ビットモードか64ビットモードか、および(GNU/Linuxの場合)要求されたアドレスに基づいてチェックを行っていましたが、要求されたアドレスと返されたアドレスを混同していました。この変更により、メモリ予約のステータスがより正確に追跡され、ランタイムのメモリ管理の堅牢性が向上します。
## コミット
commit 4ebfa8319914e1ed9727592d1fa360ce339b7597 Author: Ian Lance Taylor iant@golang.org Date: Tue Mar 25 13:22:19 2014 -0700
runtime: accurately record whether heap memory is reserved
The existing code did not have a clear notion of whether
memory has been actually reserved. It checked based on
whether in 32-bit mode or 64-bit mode and (on GNU/Linux) the
requested address, but it confused the requested address and
the returned address.
LGTM=rsc
R=rsc, dvyukov
CC=golang-codereviews, michael.hudson
https://golang.org/cl/79610043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/4ebfa8319914e1ed9727592d1fa360ce339b7597](https://github.com/golang/go/commit/4ebfa8319914e1ed9727592d1fa360ce339b7597)
## 元コミット内容
このコミットの元の内容は、Goランタイムがヒープメモリの予約状況を正確に記録するように修正することです。既存のコードは、メモリが実際に予約されたかどうかの明確な概念を持っておらず、32ビットモードか64ビットモードか、および(GNU/Linuxの場合)要求されたアドレスに基づいてチェックを行っていましたが、要求されたアドレスと返されたアドレスを混同していました。
## 変更の背景
Goランタイムは、プログラムが使用するメモリを効率的に管理するために、オペレーティングシステム(OS)から仮想メモリを要求します。このメモリ管理には、大きく分けて「予約 (reserve)」と「コミット (commit)」という2つの段階があります。
1. **予約 (Reserve)**: これは、特定の仮想アドレス範囲を将来使用するために確保するプロセスです。この段階では、物理メモリは割り当てられず、単にそのアドレス範囲が他のプロセスに割り当てられないようにマークされるだけです。OSによっては、この予約が非常に「軽い」操作であり、実際に物理メモリが割り当てられるまで厳密なチェックを行わない場合があります。特に64ビットシステムでは、非常に広大な仮想アドレス空間が存在するため、OSは実際に必要になるまで物理メモリの割り当てを遅延させることが一般的です。
2. **コミット (Commit)**: これは、予約された仮想アドレス範囲に実際に物理メモリを割り当てるプロセスです。プログラムがそのメモリにアクセスしようとすると、OSはページフォールトを処理し、必要に応じて物理ページを割り当てます。
このコミット以前のGoランタイムでは、`SysReserve`関数がメモリを予約する際に、そのメモリがOSによって実際に「予約された」のか、それとも単に「利用可能であることが確認された」だけなのかを正確に区別できていませんでした。特に64ビットシステムや、`ulimit -v`のような仮想メモリ制限が設定されている環境では、`SysReserve`が成功したと見せかけても、実際にはOSがそのアドレス空間を厳密に予約していないケースがありました。これにより、`SysMap`(予約されたメモリをコミットする関数)が後で呼び出された際に、予期せぬ問題(例えば、メモリ不足エラー)が発生する可能性がありました。
また、既存のコードは、`SysReserve`に渡された「要求されたアドレス」と、OSが実際に返した「割り当てられたアドレス」を混同していました。これは、OSが要求されたアドレスとは異なるアドレスにメモリを割り当てる可能性があるため、問題を引き起こす可能性がありました。
このコミットは、これらの問題を解決し、GoランタイムがOSからのメモリ予約のステータスをより正確に把握できるようにすることを目的としています。これにより、メモリ管理の堅牢性が向上し、特に大規模なメモリを扱うアプリケーションや、様々なOS環境での安定性が確保されます。
## 前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
1. **仮想メモリ (Virtual Memory)**:
* 現代のオペレーティングシステムが提供するメモリ管理の抽象化層です。各プロセスは、物理メモリの実際の配置とは独立した、連続した仮想アドレス空間を持っているかのように見えます。
* 仮想アドレスは、メモリ管理ユニット(MMU)によって物理アドレスに変換されます。
* これにより、プロセスは物理メモリのサイズよりも大きなメモリを使用できる(スワップ領域の利用)ようになり、複数のプロセスが互いに干渉することなくメモリを共有できるようになります。
2. **メモリの予約 (Reservation) とコミット (Commitment)**:
* **予約 (Reserve)**: 仮想アドレス空間の一部を、将来使用するために確保することです。この段階では、対応する物理メモリは割り当てられません。OSは、そのアドレス範囲が他の目的で使用されないようにマークするだけです。Windowsの`VirtualAlloc`の`MEM_RESERVE`フラグや、Unix系OSの`mmap`で`PROT_NONE`を指定する操作がこれに該当します。
* **コミット (Commit)**: 予約された仮想アドレス空間に実際に物理メモリを割り当てることです。これにより、プログラムはそのメモリ領域にデータを読み書きできるようになります。Windowsの`VirtualAlloc`の`MEM_COMMIT`フラグや、Unix系OSの`mmap`で`PROT_READ|PROT_WRITE`などの保護フラグを指定する操作がこれに該当します。
* OSによっては、予約操作が非常に「軽い」ものであり、実際に物理メモリがコミットされるまで、そのアドレス空間が本当に利用可能であるかどうかの厳密なチェックを行わない場合があります。特に64ビットシステムでは、広大な仮想アドレス空間を効率的に管理するために、このような「楽観的な」予約が行われることがあります。
3. **`mmap`システムコール (Unix-like systems)**:
* Unix系OSでメモリマッピングを行うためのシステムコールです。ファイルや匿名メモリ領域をプロセスの仮想アドレス空間にマッピングするために使用されます。
* `mmap(addr, len, prot, flags, fd, offset)`
* `addr`: マッピングを希望する開始アドレス。`NULL`の場合、OSが適切なアドレスを選択します。
* `len`: マッピングするバイト数。
* `prot`: メモリ保護フラグ(例: `PROT_NONE` (アクセス不可), `PROT_READ` (読み取り), `PROT_WRITE` (書き込み), `PROT_EXEC` (実行))。
* `flags`: マッピングのタイプや動作を制御するフラグ(例: `MAP_ANON` (匿名メモリ), `MAP_PRIVATE` (プライベートコピー), `MAP_FIXED` (`addr`を厳密に要求))。
* `PROT_NONE`と`MAP_ANON`を組み合わせて使用することで、物理メモリを割り当てずに仮想アドレス空間を予約する目的で利用されます。
4. **`VirtualAlloc` API (Windows)**:
* Windowsで仮想メモリを操作するためのAPIです。
* `VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)`
* `lpAddress`: 割り当てを希望する開始アドレス。`NULL`の場合、システムが決定します。
* `dwSize`: 割り当てる領域のサイズ。
* `flAllocationType`: 割り当てのタイプ(例: `MEM_RESERVE` (予約), `MEM_COMMIT` (コミット))。
* `flProtect`: 領域のメモリ保護(例: `PAGE_READWRITE`)。
5. **Goランタイムのメモリ管理 (MHeap, Arenas)**:
* Goランタイムは独自のヒープマネージャを持っています。これは、OSから大きな仮想メモリの塊(アリーナ、`arena`)を予約し、そのアリーナ内でオブジェクトを割り当てて管理します。
* `MHeap`構造体は、Goランタイムのグローバルなヒープの状態を管理します。これには、アリーナの開始アドレス、使用中のアドレス、終了アドレスなどが含まれます。
* `SysReserve`と`SysMap`は、GoランタイムがOSとやり取りして仮想メモリを予約・コミットするための低レベルな関数です。
## 技術的詳細
このコミットの主要な技術的変更点は、Goランタイムのメモリ予約メカニズムに「実際に予約されたかどうか」を示す`bool`型のフラグを導入したことです。
具体的には、以下の変更が行われました。
1. **`runtime·SysReserve`関数のシグネチャ変更**:
* 変更前: `void* runtime·SysReserve(void *v, uintptr nbytes)`
* 変更後: `void* runtime·SysReserve(void *v, uintptr nbytes, bool *reserved)`
* 新しい`bool *reserved`引数は、`SysReserve`が呼び出し元に、実際にメモリがOSによって予約されたかどうか(`true`)または単に利用可能であることが確認されただけか(`false`)を伝えるためのポインタです。これにより、呼び出し元はOSの挙動に基づいて後続の処理を調整できるようになります。
2. **`runtime·SysMap`関数のシグネチャ変更**:
* 変更前: `void runtime·SysMap(void *v, uintptr nbytes, uint64 *stat)`
* 変更後: `void runtime·SysMap(void *v, uintptr nbytes, bool reserved, uint64 *stat)`
* 新しい`bool reserved`引数は、`SysMap`がコミットしようとしているメモリ領域が、以前の`SysReserve`呼び出しによって実際に予約されたものなのかどうかを示します。`SysMap`はこの情報を使用して、OS固有の挙動(特に64ビットシステムでの「楽観的な」予約)を適切に処理します。例えば、`reserved`が`false`の場合、`SysMap`は`mmap`を呼び出して、その領域を実際にコミットするだけでなく、必要であれば予約も行います。
3. **`MHeap`構造体への`arena_reserved`フィールドの追加**:
* `src/pkg/runtime/malloc.h`の`MHeap`構造体に`bool arena_reserved;`フィールドが追加されました。
* このフィールドは、GoランタイムのメインヒープアリーナがOSによって実際に予約されているかどうかを追跡します。`runtime·mallocinit`および`runtime·MHeap_SysAlloc`で`SysReserve`が呼び出された際に、このフラグが設定されます。
4. **OS固有のメモリ管理コードの更新**:
* `src/pkg/runtime/mem_*.c`ファイル(Darwin, Dragonfly, FreeBSD, Linux, NaCl, NetBSD, OpenBSD, Plan9, Solaris, Windows)が、新しい`SysReserve`と`SysMap`のシグネチャに合わせて更新されました。
* これらのファイルでは、各OSの`mmap`や`VirtualAlloc`の挙動に基づいて、`reserved`フラグが適切に設定されます。
* 多くの64ビットUnix系OSでは、`SysReserve`が非常に大きなメモリ領域(例えば、4GB以上)を要求する場合、`*reserved`を`false`に設定し、単に要求されたアドレスを返すことで、実際の予約を`SysMap`に委ねるようになりました。これは、`ulimit -v`のような制限がある環境で、過剰な仮想メモリ予約によるエラーを避けるためです。
* WindowsやDarwin(macOS)など、`SysReserve`が常に実際の予約を行うOSでは、`*reserved`は常に`true`に設定されます。
* Plan9では、`SysReserve`が`SysAlloc`を呼び出すため、常に`*reserved = true`となります。
5. **ヒープ初期化とアリーナ拡張ロジックの更新**:
* `src/pkg/runtime/malloc.goc`内の`runtime·mallocinit`(ヒープ初期化)と`runtime·MHeap_SysAlloc`(アリーナ拡張)関数が、`SysReserve`の新しいシグネチャを使用し、`arena_reserved`フラグを適切に設定・利用するように変更されました。
* 特に`runtime·MHeap_SysAlloc`では、32ビットモードでのアリーナ拡張時に、`arena_reserved`フラグを考慮して`SysMap`を呼び出すようになりました。これにより、アリーナの一部が予約済みで、一部がそうでないという不整合な状態を防ぐことができます。
6. **ビットマップとスパンのマップ処理の更新**:
* `src/pkg/runtime/mgc0.c`の`runtime·MHeap_MapBits`と`src/pkg/runtime/mheap.c`の`runtime·MHeap_MapSpans`が、`SysMap`を呼び出す際に`h->arena_reserved`フラグを渡すように変更されました。これにより、これらの内部データ構造がマップされる際にも、アリーナの予約状況が正確に考慮されます。
これらの変更により、GoランタイムはOSのメモリ予約の挙動をより正確に把握し、それに基づいてメモリ管理を最適化できるようになりました。特に、64ビットシステムでの大規模なヒープの管理において、より堅牢で効率的な動作が期待されます。
## コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下のファイルに集中しています。
1. **`src/pkg/runtime/malloc.goc`**:
* `runtime·mallocinit`関数と`runtime·MHeap_SysAlloc`関数で、`runtime·SysReserve`の呼び出しに`bool *reserved`引数が追加され、その結果が`MHeap`構造体の新しいフィールド`arena_reserved`に格納されるようになりました。
* `runtime·MHeap_SysAlloc`内で`runtime·SysMap`を呼び出す際にも、`h->arena_reserved`が引数として渡されるようになりました。
2. **`src/pkg/runtime/malloc.h`**:
* `runtime·SysReserve`と`runtime·SysMap`関数のプロトタイプが変更され、新しい`bool`引数が追加されました。
* `MHeap`構造体に`bool arena_reserved;`フィールドが追加されました。
* `runtime·SysReserve`と`runtime·SysMap`のコメントが更新され、新しい`reserved`引数の意味と、OSによるメモリ予約の挙動に関する詳細が記述されました。
3. **`src/pkg/runtime/mem_*.c` (各OS固有のメモリ管理ファイル)**:
* `mem_darwin.c`, `mem_dragonfly.c`, `mem_freebsd.c`, `mem_linux.c`, `mem_nacl.c`, `mem_netbsd.c`, `mem_openbsd.c`, `mem_plan9.c`, `mem_solaris.c`, `mem_windows.c`
* これらのファイル内の`runtime·SysReserve`と`runtime·SysMap`の実装が、新しいシグネチャに合わせて変更されました。
* `runtime·SysReserve`の実装内で、OSの挙動に基づいて`*reserved`引数に`true`または`false`が設定されるようになりました。特に、64ビットシステムで大きなメモリ領域を予約する際に、OSが実際に予約を行わない可能性がある場合に`false`を設定するロジックが追加されました。
* `runtime·SysMap`の実装内で、`reserved`引数の値に基づいて、メモリをコミットする際の挙動が調整されるようになりました。`reserved`が`false`の場合、`mmap`を呼び出して実際にメモリをコミットする際に、必要に応じて予約も同時に行うような処理が追加されました。
4. **`src/pkg/runtime/mgc0.c`**:
* `runtime·MHeap_MapBits`関数で、`runtime·SysMap`の呼び出しに`h->arena_reserved`が引数として渡されるようになりました。
5. **`src/pkg/runtime/mheap.c`**:
* `runtime·MHeap_MapSpans`関数で、`runtime·SysMap`の呼び出しに`h->arena_reserved`が引数として渡されるようになりました。
これらの変更は、Goランタイムのメモリ管理の根幹に関わる部分であり、OSとのインタフェース層に新たな情報伝達メカニズムを導入することで、より正確で堅牢なメモリ管理を実現しています。
## コアとなるコードの解説
### `src/pkg/runtime/malloc.h`
```c
// SysReserve reserves address space without allocating memory.
// If the pointer passed to it is non-nil, the caller wants the
// reservation there, but SysReserve can still choose another
// location if that one is unavailable. On some systems and in some
// cases SysReserve will simply check that the address space is
// available and not actually reserve it. If SysReserve returns
// non-nil, it sets *reserved to true if the address space is
// reserved, false if it has merely been checked.
// NOTE: SysReserve returns OS-aligned memory, but the heap allocator
// may use larger alignment, so the caller must be careful to realign the
// memory obtained by SysAlloc.
//
// SysMap maps previously reserved address space for use.
// The reserved argument is true if the address space was really
// reserved, not merely checked.
//
// SysFault marks a (already SysAlloc'd) region to fault
// if accessed. Used only for debugging the runtime.
void* runtime·SysReserve(void *v, uintptr nbytes, bool *reserved);
void runtime·SysMap(void *v, uintptr nbytes, bool reserved, uint64 *stat);
// ... (MHeap struct definition) ...
struct MHeap
{
// ...
byte *arena_start;
byte *arena_used;
byte *arena_end;
bool arena_reserved; // 新しく追加されたフィールド
// ...
};
SysReserve
とSysMap
の関数シグネチャが変更され、reserved
引数が追加されました。SysReserve
のコメントが更新され、*reserved
引数が「実際に予約されたか」または「単にチェックされただけか」を示すことが明記されました。SysMap
のコメントも更新され、reserved
引数が「実際に予約された領域か」を示すことが明記されました。MHeap
構造体にarena_reserved
という新しいbool
フィールドが追加されました。これは、GoランタイムのメインヒープアリーナがOSによって実際に予約されているかどうかを追跡します。
src/pkg/runtime/malloc.goc
void runtime·mallocinit(void) {
// ...
bool reserved; // 新しいローカル変数
// ...
reserved = false; // 初期化
// ...
// 64-bit buildの場合の初期アリーナ予約
p = runtime·SysReserve(p, p_size, &reserved); // SysReserveに&reservedを渡す
// ...
// 32-bit buildの場合の初期アリーナ予約
p = runtime·SysReserve(p, p_size, &reserved); // SysReserveに&reservedを渡す
// ...
runtime·mheap.arena_reserved = reserved; // MHeapに予約状態を保存
// ...
}
byte* runtime·MHeap_SysAlloc(MHeap *h, uintptr n) {
// ...
bool reserved; // 新しいローカル変数
// ...
if(n > h->arena_end - h->arena_used) {
// 32-bitモードでアリーナを拡張する場合
// ...
p = runtime·SysReserve(h->arena_end, p_size, &reserved); // SysReserveに&reservedを渡す
if(p == h->arena_end) {
h->arena_end = new_end;
h->arena_reserved = reserved; // MHeapに予約状態を保存
} else if(p+p_size <= h->arena_start + MaxArena32) {
// ...
h->arena_reserved = reserved; // MHeapに予約状態を保存
}
// ...
}
if(n <= h->arena_end - h->arena_used) {
// 既存の予約からメモリをマップする場合
p = h->arena_used;
runtime·SysMap(p, n, h->arena_reserved, &mstats.heap_sys); // SysMapにh->arena_reservedを渡す
h->arena_used += n;
// ...
}
// ...
}
runtime·mallocinit
とruntime·MHeap_SysAlloc
で、SysReserve
の呼び出しが更新され、reserved
変数のアドレスが渡されるようになりました。SysReserve
から返されたreserved
の値が、runtime·mheap.arena_reserved
に格納されるようになりました。これにより、ヒープアリーナが実際に予約されているかどうかの状態がグローバルに保持されます。runtime·MHeap_SysAlloc
内でSysMap
を呼び出す際に、h->arena_reserved
の値が引数として渡されるようになりました。これは、メモリをコミットする際に、その領域が実際に予約されているかどうかの情報に基づいてOS固有の処理を行うためです。
src/pkg/runtime/mem_linux.c
(Linux固有の例)
void* runtime·SysReserve(void *v, uintptr n, bool *reserved) {
void *p;
// On 64-bit, people with ulimit -v set complain if we reserve too
// much address space. Instead, assume that the reservation is okay
// if we can reserve at least 64K and check the assumption in SysMap.
// Only user-mode Linux (UML) rejects these requests.
if(sizeof(void*) == 8 && n > 1LL<<32) { // 64ビットで非常に大きな領域を予約する場合
// 64KBだけmmapしてみて、成功すれば残りはSysMapで処理する
p = mmap_fixed(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
if (p != v) {
if(p >= (void*)4096)
runtime·munmap(p, 64<<10);
return nil;
}
runtime·munmap(p, 64<<10);
*reserved = false; // 実際に予約されていないことを示す
return v; // 要求されたアドレスを返す
}
p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
if((uintptr)p < 4096)
return nil;
*reserved = true; // 実際に予約されたことを示す
return p;
}
void runtime·SysMap(void *v, uintptr n, bool reserved, uint64 *stat) {
void *p;
runtime·xadd64(stat, n);
// On 64-bit, we don't actually have v reserved, so tread carefully.
if(!reserved) { // 予約されていない場合
p = mmap_fixed(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0); // 予約とコミットを同時に行う
if(p == (void*)ENOMEM)
runtime·throw("runtime: out of memory");
if(p != v)
runtime·throw("runtime: SysMap: cannot map at address");
} else { // 予約されている場合
p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0); // コミットのみ
if(p == (void*)ENOMEM)
runtime·throw("runtime: out of memory");
if(p != v)
runtime·throw("runtime: SysMap: cannot map at address");
}
}
runtime·SysReserve
では、64ビットシステムで非常に大きなメモリ領域(4GB以上)を予約しようとする場合、OSが実際に予約を行わない可能性があるため、*reserved
をfalse
に設定し、要求されたアドレスをそのまま返します。これは、ulimit -v
のような制限がある環境での問題を回避するためです。それ以外の場合は、mmap
が成功すれば*reserved
をtrue
に設定します。runtime·SysMap
では、reserved
引数の値に基づいて挙動が変わります。!reserved
(予約されていない場合):mmap_fixed
を呼び出し、PROT_READ|PROT_WRITE
を指定して、予約とコミットを同時に行います。これは、SysReserve
が実際の予約を行わなかった場合に、SysMap
がその役割を果たすためです。reserved
(予約されている場合):mmap
を呼び出し、MAP_FIXED
フラグを使用して、既に予約されたアドレスに物理メモリをコミットします。
これらの変更により、GoランタイムはOSのメモリ予約の挙動をより正確に把握し、それに基づいてメモリ管理を最適化できるようになりました。特に、64ビットシステムでの大規模なヒープの管理において、より堅牢で効率的な動作が期待されます。
関連リンク
- Goのメモリ管理に関する公式ドキュメントやブログ記事(コミット当時のもの、または関連する概念を説明するもの)
- Goのメモリ管理の進化に関する記事: https://go.dev/blog/go1.14-memory-management (これはコミットより新しいですが、Goのメモリ管理の概念を理解するのに役立ちます)
- Goのガベージコレクションに関する記事: https://go.dev/blog/go15gc (これもコミットより新しいですが、メモリ管理の文脈で関連します)
mmap
システムコールに関するmanページやドキュメントVirtualAlloc
APIに関するMicrosoftのドキュメント
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commit/4ebfa8319914e1ed9727592d1fa360ce339b7597
- Goのコードレビューシステム (Gerrit): https://golang.org/cl/79610043
- 仮想メモリ、メモリ予約、コミットに関する一般的なOSのドキュメントや解説記事 (例: Wikipedia, OSの教科書など)
mmap
に関するLinux man page:man 2 mmap
VirtualAlloc
に関するMicrosoft Learn: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc- Goのソースコード (特に
src/pkg/runtime
ディレクトリ) - GoのIssue Tracker (関連するバグ報告や議論がある場合)