[インデックス 12486] ファイルの概要
このコミットは、Goランタイムの32ビットアロケータにおけるアリーナサイズ拡張の試みに関するものです。具体的には、メモリ確保要求があった際に、既存のアリーナの限界に達していない場合でも、より積極的にアリーナのサイズを拡張しようとすることで、ランダムなメモリマッピングに頼る前に連続したメモリ領域を確保しやすくする変更です。これにより、特に32ビット環境でのメモリ確保の堅牢性と効率が向上することが期待されます。
コミット
commit 3dcedb620cd0900be6d6de44f66c9b7eb82f2dca
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Wed Mar 7 14:21:45 2012 -0500
runtime: try extending arena size in 32-bit allocator.
If it didn't reach the limit, we can try extending the arena
before resorting to random memory mappings and praying for the
kernel to be kind.
Fixes #3173.
R=rsc, rsc
CC=golang-dev
https://golang.org/cl/5725045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3dcedb620cd0900be6d6de44f66c9b7eb82f2dca
元コミット内容
runtime: try extending arena size in 32-bit allocator.
If it didn't reach the limit, we can try extending the arena
before resorting to random memory mappings and praying for the
kernel to be kind.
Fixes #3173.
R=rsc, rsc
CC=golang-dev
https://golang.org/cl/5725045
変更の背景
この変更の背景には、32ビットシステムにおけるGoランタイムのメモリ管理の課題があります。32ビットシステムでは、利用可能な仮想アドレス空間が4GBに制限されており、連続した大きなメモリブロックを確保することが難しい場合があります。Goのランタイムは、ヒープ領域を「アリーナ」と呼ばれる連続したメモリブロックで管理しており、効率的なメモリ割り当てのためにこのアリーナを拡張していく必要があります。
従来の32ビットアロケータでは、アリーナの現在の予約済み領域が不足した場合に、すぐにオペレーティングシステム(OS)に対して新しいメモリ領域を要求していました。しかし、この要求が必ずしも既存のアリーナに隣接する連続した領域として満たされるとは限りません。OSがランダムなアドレスにメモリをマッピングする可能性があり、これによりアリーナが断片化したり、利用可能なアドレス空間を効率的に使い切れない問題が発生していました。
このコミットは、このような状況を改善するために導入されました。アリーナの現在の予約済み領域が不足しそうになった場合でも、すぐにOSに新しいメモリ領域を要求するのではなく、まず既存のアリーナの「限界」まで拡張を試みることで、より連続性の高いメモリ確保を促進し、メモリ断片化のリスクを低減することを目的としています。コミットメッセージにある「Fixes #3173」は、この変更が特定のバグや問題(おそらく32ビット環境でのメモリ確保の失敗や非効率性に関連するもの)を解決することを示唆しています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムのメモリ管理に関する前提知識が必要です。
- Goランタイムのメモリ管理: Goは独自のガベージコレクタ(GC)とメモリマネージャを持っています。プログラムがメモリを要求すると、GoランタイムはOSからメモリを確保し、それを管理します。
- ヒープ (Heap): プログラムが動的に確保するメモリ領域です。Goでは、オブジェクトのほとんどがヒープに割り当てられます。
- アリーナ (Arena): Goのメモリマネージャがヒープを管理するために使用する、連続した仮想メモリ空間の大きなブロックです。Goは、このアリーナを細かく分割してオブジェクトに割り当てます。アリーナは必要に応じて拡張されます。
MHeap
構造体: Goランタイムのヒープ全体を管理する主要なデータ構造です。この構造体には、アリーナの開始アドレス (arena_start
)、終了アドレス (arena_end
)、現在使用中のアドレス (arena_used
) などが含まれます。MHeap_SysAlloc
関数:MHeap
構造体に関連付けられた関数で、GoランタイムがOSから新しいメモリ領域をシステムコール(SysReserve
など)を介して要求する際に呼び出されます。SysReserve
とSysMap
: GoランタイムがOSからメモリを確保する際に使用する低レベルのシステムコールです。SysReserve
: 仮想アドレス空間を予約しますが、物理メモリはまだ割り当てません。SysMap
: 予約された仮想アドレス空間に物理メモリをマッピングし、実際に使用可能にします。
- 32ビット環境のメモリ制限: 32ビットシステムでは、仮想アドレス空間が2^32バイト(4GB)に制限されます。このうち、通常はユーザープロセスが利用できるのは2GBまたは3GB程度であり、連続した大きなメモリブロックを確保することが難しいという特性があります。
MaxArena32
: 32ビット環境におけるアリーナの最大サイズまたは最大アドレス範囲を定義する定数です。この値は、32ビットアドレス空間の制約内でGoランタイムが利用できるアリーナの限界を示します。
技術的詳細
このコミットの技術的な核心は、runtime·MHeap_SysAlloc
関数におけるメモリ確保ロジックの変更です。
変更前は、MHeap_SysAlloc
が呼び出された際に、要求されたメモリ量 n
が現在のアリーナの未使用領域 (h->arena_end - h->arena_used
) を超える場合、すぐにOSに対して新しいメモリ領域を要求していました。この際、OSが返すメモリ領域が既存のアリーナに隣接している保証はなく、結果としてアリーナが断片化する可能性がありました。
変更後は、if(n > h->arena_end - h->arena_used)
の条件が真(つまり、現在のアリーナの未使用領域では足りない)の場合に、以下の新しいロジックが追加されました。
- 32ビットモードの確認: コメント
// We are in 32-bit mode, maybe we didn't use all possible address space yet.
から、このロジックが32ビット環境に特化していることがわかります。 - 必要な追加領域の計算:
needed = (uintptr)h->arena_used + n - (uintptr)h->arena_end;
これは、現在使用中の領域 (h->arena_used
) に要求されたサイズn
を加えた合計が、現在のアリーナの終了アドレス (h->arena_end
) をどれだけ超えるかを計算しています。つまり、アリーナをどこまで拡張する必要があるかを算出しています。 - アリーナサイズを256MBの倍数に丸める:
needed = (needed + (256<<20) - 1) & ~((256<<20)-1);
256<<20
は256MBを表します。この行は、needed
を256MBの倍数に切り上げています。これは、OSからのメモリ確保が通常、特定の粒度(ページサイズやより大きなブロックサイズ)で行われるため、効率的なメモリ管理のために行われる一般的なプラクティスです。 - 新しいアリーナの終了アドレスの計算:
new_end = h->arena_end + needed;
現在のアリーナの終了アドレスに、計算された追加領域を加算し、新しいアリーナの終了アドレスを決定します。 MaxArena32
の範囲内であるかチェック:if(new_end <= h->arena_start + MaxArena32)
計算されたnew_end
が、32ビット環境で許容されるアリーナの最大範囲 (h->arena_start + MaxArena32
) を超えていないかを確認します。これにより、アドレス空間の限界を超えてメモリを予約しようとすることを防ぎます。SysReserve
によるメモリ予約の試行:p = runtime·SysReserve(h->arena_end, new_end - h->arena_end);
h->arena_end
からnew_end
までの領域をOSに予約するよう試みます。これは、既存のアリーナの直後に連続したメモリを確保しようとする試みです。- 予約成功の確認とアリーナの更新:
if(p == h->arena_end) h->arena_end = new_end;
SysReserve
が成功し、かつ予約されたアドレスp
が期待通り既存のアリーナの終了アドレスh->arena_end
と一致した場合(つまり、連続した領域が確保できた場合)、h->arena_end
を新しい終了アドレスnew_end
に更新します。これにより、アリーナが拡張されます。
この新しいロジックにより、32ビット環境でメモリが不足した場合でも、まず既存のアリーナを可能な限り拡張しようとします。これにより、OSがランダムなアドレスにメモリをマッピングする「祈るような」状況を避け、より連続性の高いメモリ領域を確保できる可能性が高まります。
また、エラーメッセージの変更も行われています。
変更前: runtime·printf("runtime: memory allocated by OS not in usable range\n");
変更後: runtime·printf("runtime: memory allocated by OS (%p) not in usable range [%p,%p)\\n", p, h->arena_start, h->arena_start+MaxArena32);
これにより、メモリが使用可能な範囲外に割り当てられた場合に、より詳細なデバッグ情報(割り当てられたアドレス p
と、使用可能な範囲 [h->arena_start, h->arena_start+MaxArena32)
)が出力されるようになり、問題の診断が容易になります。
コアとなるコードの変更箇所
src/pkg/runtime/malloc.goc
ファイルの runtime·MHeap_SysAlloc
関数に以下の変更が加えられています。
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -371,6 +371,22 @@ runtime·MHeap_SysAlloc(MHeap *h, uintptr n)
{
byte *p;
+ if(n > h->arena_end - h->arena_used) {
+ // We are in 32-bit mode, maybe we didn't use all possible address space yet.
+ // Reserve some more space.
+ byte *new_end;
+ uintptr needed;
+
+ needed = (uintptr)h->arena_used + n - (uintptr)h->arena_end;
+ // Round wanted arena size to a multiple of 256MB.
+ needed = (needed + (256<<20) - 1) & ~((256<<20)-1);
+ new_end = h->arena_end + needed;
+ if(new_end <= h->arena_start + MaxArena32) {
+ p = runtime·SysReserve(h->arena_end, new_end - h->arena_end);
+ if(p == h->arena_end)
+ h->arena_end = new_end;
+ }
+ }
if(n <= h->arena_end - h->arena_used) {
// Keep taking from our reservation.
p = h->arena_used;
@@ -392,7 +408,8 @@ runtime·MHeap_SysAlloc(MHeap *h, uintptr n)
return nil;
if(p < h->arena_start || p+n - h->arena_start >= MaxArena32) {
- runtime·printf("runtime: memory allocated by OS not in usable range\n");
+ runtime·printf("runtime: memory allocated by OS (%p) not in usable range [%p,%p)\\n",
+ p, h->arena_start, h->arena_start+MaxArena32);
runtime·SysFree(p, n);
return nil;
}
コアとなるコードの解説
追加されたコードブロックは、runtime·MHeap_SysAlloc
関数の冒頭に配置されています。これは、OSから新しいメモリを要求する主要なロジックの前に実行されます。
-
if(n > h->arena_end - h->arena_used)
: この条件は、要求されたメモリ量n
が、現在のアリーナの予約済み領域の残量 (h->arena_end - h->arena_used
) を超えている場合に真となります。つまり、現在のアリーナの予約済み領域だけでは、今回のメモリ確保要求を満たせないことを意味します。 -
// We are in 32-bit mode, maybe we didn't use all possible address space yet.
: このコメントは、この新しいロジックが32ビットシステムに特化していることを示しています。32ビットシステムではアドレス空間が限られているため、連続したメモリを確保するための戦略がより重要になります。 -
byte *new_end; uintptr needed;
: 新しいアリーナの終了アドレスを格納するポインタnew_end
と、追加で必要となるメモリ量を格納するneeded
変数を宣言しています。 -
needed = (uintptr)h->arena_used + n - (uintptr)h->arena_end;
: この計算は、現在使用中のアリーナの末尾 (h->arena_used
) から、要求されたサイズn
を加えた合計が、現在のアリーナの予約済み領域の末尾 (h->arena_end
) をどれだけ超えるかを算出しています。これが、アリーナを拡張するために必要な最小限の追加メモリ量となります。 -
needed = (needed + (256<<20) - 1) & ~((256<<20)-1);
:needed
の値を256MBの倍数に切り上げています。256<<20
は256 * 1024 * 1024
であり、256メガバイトを表します。この操作は、メモリ確保の粒度を揃え、OSとのやり取りを効率化するために行われます。 -
new_end = h->arena_end + needed;
: 現在のアリーナの予約済み領域の末尾に、切り上げられたneeded
を加算し、アリーナの新しい終了アドレスnew_end
を計算します。 -
if(new_end <= h->arena_start + MaxArena32)
: 計算されたnew_end
が、32ビット環境でGoランタイムが利用できるアリーナの最大アドレス範囲 (h->arena_start + MaxArena32
) を超えていないかを確認します。これにより、有効なアドレス空間内でのみ拡張を試みます。 -
p = runtime·SysReserve(h->arena_end, new_end - h->arena_end);
:runtime·SysReserve
を呼び出し、現在のアリーナの終了アドレスh->arena_end
からnew_end
までの領域をOSに予約するよう試みます。この関数は、仮想アドレス空間を予約しますが、まだ物理メモリは割り当てません。重要なのは、既存のアリーナの直後に連続した領域を予約しようとしている点です。 -
if(p == h->arena_end) h->arena_end = new_end;
:SysReserve
が成功し、かつ予約されたアドレスp
が期待通りh->arena_end
と一致した場合(つまり、既存のアリーナに連続する形でメモリが予約できた場合)、h->arena_end
をnew_end
に更新します。これにより、Goランタイムはアリーナが拡張されたと認識し、その新しい領域を将来のメモリ割り当てに利用できるようになります。
この一連のロジックにより、32ビット環境でのメモリ確保において、OSがランダムなアドレスにメモリをマッピングする前に、まず既存のアリーナを可能な限り連続的に拡張しようとすることで、メモリの断片化を抑制し、より効率的なメモリ利用を目指しています。
エラーメッセージの変更は、デバッグ時の情報量を増やし、問題の特定を容易にするための改善です。
関連リンク
- Go CL (Code Review) ページ: https://golang.org/cl/5725045
参考にした情報源リンク
- Goのメモリ管理に関する一般的な情報源 (例: Goの公式ドキュメント、Goのランタイムに関するブログ記事など)
- 32ビットシステムにおける仮想メモリの制約に関する情報源
runtime/malloc.goc
のソースコード (GoのGitHubリポジトリ)- GoのIssueトラッカー (ただし、#3173の具体的な内容はコミットメッセージからのみ推測)