[インデックス 1287] ファイルの概要
このコミットは、Go言語の初期のメモリ管理システム、特にmalloc
の実装における重要な修正と最適化に焦点を当てています。usr/rsc/mem
ディレクトリは、Goランタイムのメモリ割り当てメカニズムを構成するファイル群を含んでいます。
usr/rsc/mem/Makefile
: メモリ管理関連のコンパイルとリンクを定義するMakefile。usr/rsc/mem/allocator.go
: Goランタイムのメモリ割り当てに関するグローバル変数や関数をエクスポートするGoファイル。usr/rsc/mem/malloc.c
: C言語で書かれた主要なメモリ割り当てロジックを含むファイル。malloc
やfree
といった基本的なメモリ操作を実装しています。usr/rsc/mem/malloc.h
:malloc.c
で使用されるデータ構造や関数の宣言を含むヘッダーファイル。usr/rsc/mem/stack.c
: 新規追加されたファイルで、スタック割り当てに関連するロジックをカプセル化しています。usr/rsc/mem/triv.c
: 比較的単純な(trivially)メモリ割り当てを行うためのユーティリティ関数を含むファイル。
コミット
commit c1868bc89debde4b36577cc4b01513b7685fe0a1
Author: Russ Cox <rsc@golang.org>
Date: Thu Dec 4 21:04:26 2008 -0800
malloc fixes.
can run peano 10 in 100 MB (instead of 1+ GB) of memory
when linking against this.
can run peano 11 in 1 GB of memory now.
R=r
DELTA=100 (44 added, 44 deleted, 12 changed)
OCL=20504
CL=20553
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c1868bc89debde4b36577cc4b01513b7685fe0a1
元コミット内容
malloc fixes.
can run peano 10 in 100 MB (instead of 1+ GB) of memory
when linking against this.
can run peano 11 in 1 GB of memory now.
変更の背景
このコミットの主な背景は、Goランタイムのメモリ使用量を大幅に削減することにありました。コミットメッセージに明記されているように、「peano 10」というベンチマーク(おそらくPeano曲線の生成など、再帰的でメモリを大量に消費する処理)を実行する際に、以前は1GB以上ものメモリを消費していたものが、この修正によって100MBにまで削減されました。さらに、「peano 11」も1GBのメモリで実行可能になったとあります。
これは、Go言語がまだ開発の初期段階にあった2008年当時、メモリ効率が重要な課題であったことを示しています。特に、ガベージコレクションを持つ言語において、不必要なメモリの確保や解放の遅延は、アプリケーションのパフォーマンスとスケーラビリティに直接影響します。このコミットは、メモリ割り当てのアルゴリズムと実装における非効率性を特定し、それを改善することで、より少ないメモリでより大きな問題を扱えるようにすることを目指しました。
具体的な問題点としては、おそらく以下のいずれか、または複数の組み合わせが考えられます。
- 過剰なメモリ割り当て: 実際に必要とされる量よりも多くのメモリが割り当てられていた。
- メモリの断片化: 割り当てと解放が繰り返されることで、利用可能なメモリが小さなブロックに分断され、大きな連続したメモリ領域を確保できなくなっていた。
- 非効率な解放: 解放されたメモリがすぐに再利用されず、システムに返却されるまでに時間がかかっていた。
- デバッグ出力のオーバーヘッド: 開発中に埋め込まれたデバッグ用の
prints
文が、パフォーマンスやメモリ使用量に影響を与えていた可能性。
これらの問題を解決し、Goプログラムがより少ないリソースで動作できるようにすることが、このコミットの重要な動機となっています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
1. メモリ管理の基本
- ヒープ (Heap): プログラムが実行時に動的にメモリを割り当てる領域。
malloc
やnew
といった関数/キーワードで確保されるメモリは通常ヒープに配置されます。 - スタック (Stack): 関数呼び出しやローカル変数のために使用されるメモリ領域。LIFO (Last-In, First-Out) 構造で、関数の呼び出しと終了に伴って自動的に割り当て・解放されます。
- メモリ割り当て (Memory Allocation): プログラムが実行時に必要なメモリをシステムから取得するプロセス。
- メモリ解放 (Memory Deallocation): 不要になったメモリをシステムに返却するプロセス。
2. malloc
とfree
malloc
(memory allocation): C言語の標準ライブラリ関数で、指定されたサイズのメモリブロックをヒープから割り当て、そのブロックの先頭へのポインタを返します。free
:malloc
によって割り当てられたメモリブロックを解放し、再利用可能にする関数。
3. ページングと仮想メモリ
- ページ (Page): オペレーティングシステムがメモリを管理する際の最小単位。通常4KBなどの固定サイズです。
- 仮想メモリ (Virtual Memory): 物理メモリの制約を超えて、より大きなメモリ空間をプログラムに提供する技術。ディスク上のスワップ領域と物理メモリを組み合わせて使用します。
mmap
(memory map): Unix系システムコールの一つで、ファイルやデバイス、または匿名メモリ領域をプロセスの仮想アドレス空間にマッピングするために使用されます。メモリ割り当てのバックエンドとして使われることがあります。
4. ガベージコレクション (Garbage Collection - GC)
Go言語はガベージコレクタを持つ言語です。GCは、プログラムが動的に割り当てたメモリのうち、もはや到達不可能(参照されていない)になったものを自動的に解放する仕組みです。このコミットは直接GCのアルゴリズムを変更するものではありませんが、GCが効率的に動作するためには、基盤となるメモリ割り当て・解放の効率が非常に重要です。不必要なメモリの保持はGCの負担を増やし、パフォーマンスを低下させます。
5. Goランタイムのメモリ管理の概念 (初期段階)
Goのランタイムは、OSから直接メモリを要求し(通常はmmap
などを使用)、それを独自のヒープとして管理します。このヒープは、さらに小さなブロック(オブジェクト)に分割され、Goプログラムのオブジェクト割り当て要求に応じます。
- Span: Goのメモリ管理における基本的な単位の一つ。連続したページ(通常は8KBの倍数)の集合で、Goのオブジェクトを格納するために使用されます。
- Size Class: 割り当てられるオブジェクトのサイズに応じて、メモリブロックを分類するための仕組み。例えば、小さなオブジェクトは特定のサイズクラスに属するブロックから割り当てられ、大きなオブジェクトは別の方法で割り当てられます。これにより、メモリの断片化を減らし、割り当て効率を向上させます。
- Central Cache: 複数のゴルーチン(Goの軽量スレッド)間で共有されるメモリブロックのプール。ゴルーチンがメモリを要求する際に、まずローカルなキャッシュ(Mcache)をチェックし、なければCentral Cacheから取得します。
技術的詳細
このコミットにおける技術的詳細は、主にGoランタイムのメモリ割り当て器(アロケータ)の効率改善にあります。
1. malloc.c
の変更点
- デバッグ出力の削除/コメントアウト: 多くの
prints
やsys·printint
といったデバッグ出力がコメントアウトまたは削除されています。これらは開発段階でメモリ割り当ての挙動を追跡するために使用されていましたが、本番環境ではオーバーヘッドとなり、パフォーマンスとメモリ使用量に悪影響を与えます。例えば、allocspan
やcentralgrab
、allocsmall
、alloclarge
、free
関数内のデバッグ出力が対象となっています。prints("Chop span")
->//if(s->length > npage) printf("Chop span %D for %d\\n", s->length, npage);
prints("New span ")
->//printf("New span %d for %d\\n", allocnpage, npage);
prints("sizetoclass ")
->printf("sizetoclass %d = %d want %d\\n", n, sizetoclass(n), i);
prints("testsizetoclass stopped at ")
->printf("testsizetoclass stopped at %d\\n", n);
prints("New Class ")
->//printf("New class %d\\n", cl);
prints("Alloc span ")
->//printf("Alloc span %d\\n", np);
prints(" -> ")
->printf("%d -> %d\\n", n, cl);
prints("Free big ")
->//printf("Free big %D\\n", s->length);
//printf("centralgrab for %d\\n", cl);
や//printf("alloc from cl %d\\n", cl);
の追加。//printf("Free siz %d cl %d\\n", siz, s->cl);
の追加。
allocator·allocated
の導入:alloc
関数内で、実際に割り当てられたメモリの総量を追跡するためのallocator·allocated
変数が導入されています。これは、allocsmall
とalloclarge
の両方で更新され、free
関数でも解放時に減算されます。これにより、Goランタイムが現在どれだけのメモリをアプリケーションに割り当てているかを正確に把握できるようになります。これは、メモリ使用量の最適化やデバッグに不可欠な情報です。allocsmall
の呼び出し前にallocator·allocated += classtosize[cl];
を追加。alloclarge
の呼び出し前にallocator·allocated += (uint64)np<<PageShift;
を追加。free
関数内で大きなスパンを解放する際にallocator·allocated -= s->length << PageShift;
を追加。free
関数内で小さなオブジェクトを解放する際にallocator·allocated -= siz;
を追加。
- スタック割り当てロジックの分離:
allocstack
とfreestack
関数がmalloc.c
から削除され、新しく追加されたstack.c
ファイルに移動されました。これは、コードのモジュール化と関心事の分離を促進します。スタックの割り当てと解放は、一般的なヒープ割り当てとは異なる特性を持つため、専用のファイルで管理することで、コードの可読性と保守性が向上します。
2. allocator.go
の変更点
export var allocated int64
:allocator·allocated
変数をGo側から参照できるようにエクスポートしています。これにより、Goプログラムやランタイムの他の部分から、現在割り当てられているメモリの総量にアクセスできるようになります。
3. malloc.h
の変更点
extern int64 allocator·allocated;
:allocator·allocated
変数の宣言を追加し、Cコード間で共有できるようにしています。void* alloc(int32);
とvoid free(void*);
の関数プロトタイプ宣言を追加。これは、これらの関数が外部から呼び出されることを明示するためです。
4. stack.c
の新規追加
stackalloc(uint32 n)
: 指定されたサイズのスタックメモリを割り当てる関数。内部でalloc(n)
を呼び出しています。stackfree(void *v)
: 割り当てられたスタックメモリを解放する関数。内部でfree(v)
を呼び出しています。- これらの関数は、Goランタイムがゴルーチンのスタックを動的に管理するために使用されます。スタックの動的な拡張・縮小は、Goの並行処理モデルにおいて重要な機能です。
5. triv.c
の変更点
- メモリフットプリントの追跡と出力:
trivalloc
関数内で、allocator·footprint
(OSから取得した総メモリ量)とallocator·allocated
(実際に割り当てられたメモリ量)を比較し、メモリフットプリントが大きく変化した場合にデバッグ情報を出力するロジックが追加されています。uint64 oldfoot;
の追加。oldfoot = allocator·footprint;
で古いフットプリントを保存。if((oldfoot>>24) != (allocator·footprint>>24))
で、フットプリントが16MB(2^24バイト)の倍数で変化した場合にメッセージを出力。printf("memory footprint = %D MB for %D MB\\n", allocator·footprint>>20, allocator·allocated>>20);
で、フットプリントと割り当て済みメモリをMB単位で表示。
- メモリ不足のチェック:
if(allocator·footprint >= 2LL<<30)
で、メモリフットプリントが2GBを超えた場合に「out of memory」メッセージを出力し、プログラムを終了するチェックが追加されています。これは、メモリリークや過剰なメモリ使用を早期に検出するための安全策です。
これらの変更は、Goランタイムのメモリ管理をより堅牢で効率的にするための初期のステップであり、特にメモリ使用量の削減とデバッグ能力の向上に貢献しています。
コアとなるコードの変更箇所
usr/rsc/mem/malloc.c
--- a/usr/rsc/mem/malloc.c
+++ b/usr/rsc/mem/malloc.c
@@ -107,13 +107,7 @@ allocspan(int32 npage)
if(s->length >= npage) {
*l = s->next;
s->next = nil;
-if(s->length > npage) {
-prints("Chop span");
-sys·printint(s->length);
-prints(" for ");
-sys·printint(npage);
-prints("\n");
-}
+//if(s->length > npage) printf("Chop span %D for %d\\n", s->length, npage);\n
goto havespan;
}
}
@@ -125,11 +119,7 @@ prints("\n");
if(allocnpage < (1<<20>>PageShift)) // TODO: Tune
allocnpage = (1<<20>>PageShift);
s->length = allocnpage;
-prints("New span ");
-sys·printint(allocnpage);
-prints(" for ");
-sys·printint(npage);
-prints("\n");
+//printf("New span %d for %d\\n", allocnpage, npage);\n
s->base = trivalloc(allocnpage<<PageShift);
insertspan(s);
@@ -237,21 +227,13 @@ allocator·testsizetoclass(void)
for(i=0; i<nelem(classtosize); i++) {
for(; n <= classtosize[i]; n++) {
if(sizetoclass(n) != i) {
-\t\t\t\tprints("sizetoclass ");
-\t\t\t\tsys·printint(n);
-\t\t\t\tprints(" = ");
-\t\t\t\tsys·printint(sizetoclass(n));
-\t\t\t\tprints(" want ");
-\t\t\t\tsys·printint(i);
-\t\t\t\tprints("\n");
+\t\t\t\tprintf("sizetoclass %d = %d want %d\\n", n, sizetoclass(n), i);\n
throw("testsizetoclass");
}
}
}
if (n != 32768+1) {
-\t\tprints("testsizetoclass stopped at ");
-\t\t\tsys·printint(n);
-\t\tprints("\n");
+\t\tprintf("testsizetoclass stopped at %d\\n", n);\n
throw("testsizetoclass");
}
}
@@ -274,20 +256,19 @@ centralgrab(int32 cl, int32 *pn)
}
chunk = (chunk+PageMask) & ~PageMask;
s = allocspan(chunk>>PageShift);
-prints("New Class ");
-sys·printint(cl);
-prints("\n");
+//printf("New class %d\\n", cl);\n
s->state = SpanInUse;\n
s->cl = cl;\n
siz = classtosize[cl];\n
n = chunk/siz;\n
p = s->base;\n
+//printf("centralgrab cl=%d siz=%d n=%d\\n", cl, siz, n);\n
for(i=0; i<n-1; i++) {
*(void**)p = p+siz;\n
p += siz;\n
}\n *pn = n;\n-\treturn p;\n+\treturn s->base;\n }\n \n // Allocate a small object of size class cl.\n@@ -305,11 +286,13 @@ allocsmall(int32 cl)\n if(p == nil) {\n // otherwise grab some blocks from central cache.\n lock(¢ral);\n+//printf("centralgrab for %d\\n", cl);\n p = centralgrab(cl, &n);\n // TODO: update local counters using n\n unlock(¢ral);\n }\n \n+//printf("alloc from cl %d\\n", cl);\n // advance linked list.\n m->freelist[cl] = *p;\n \n@@ -327,9 +310,7 @@ alloclarge(int32 np)\n Span *s;\n \n lock(¢ral);\n-//prints("Alloc span ");\n-//sys·printint(np);\n-//prints("\n");\n+//printf("Alloc span %d\\n", np);\n s = allocspan(np);\n unlock(¢ral);\n s->state = SpanInUse;\n@@ -346,17 +327,16 @@ alloc(int32 n)\n if(n < LargeSize) {\n cl = sizetoclass(n);\n if(cl < 0 || cl >= SmallFreeClasses) {\n-\t\t\tsys·printint(n);\n-\t\t\tprints(" -> ");\n-\t\t\tsys·printint(cl);\n-\t\t\tprints("\n");\n+\t\t\tprintf("%d -> %d\\n", n, cl);\n throw("alloc - logic error");\n }\n-\t\treturn allocsmall(sizetoclass(n));\n+\t\tallocator·allocated += classtosize[cl];\n+\t\treturn allocsmall(cl);\n }\n \n // count number of pages; careful about overflow for big n.\n np = (n>>PageShift) + (((n&PageMask)+PageMask)>>PageShift);\n+\tallocator·allocated += (uint64)np<<PageShift;\n return alloclarge(np);\n }\n \n@@ -386,9 +366,8 @@ free(void *v)\n // TODO: For large spans, maybe just return the\n // memory to the operating system and let it zero it.\n sys·memclr(s->base, s->length << PageShift);\n-//prints("Free big ");\n-//sys·printint(s->length);\n-//prints("\n");\n+//printf("Free big %D\\n", s->length);\n+\t\tallocator·allocated -= s->length << PageShift;\n lock(¢ral);\n freespan(s);\n unlock(¢ral);\n@@ -403,9 +382,11 @@ free(void *v)\n \n // Zero and add to free list.\n sys·memclr(v, siz);\n+\tallocator·allocated -= siz;\n p = v;\n *p = m->freelist[s->cl];\n m->freelist[s->cl] = p;\n+//printf("Free siz %d cl %d\\n", siz, s->cl);\n }\n \n void\n@@ -423,21 +404,3 @@ allocator·memset(byte *v, int32 c, int32 n)\n v[i] = c;\n }\n \n-// Allocate stack segment.\n-// Must be done without holding locks, because\n-// calling any function might trigger another stack segment allocation.\n-void*\n-allocstack(int32 n)\n-{\n-// TODO\n- USED(n);\n- return nil;\n-}\n-\n-void\n-freestack(void *v)\n-{\\n-// TODO\n- USED(v);\n-}\n-\ndiff --git a/usr/rsc/mem/malloc.h b/usr/rsc/mem/malloc.h
index aa3bed2c6b..dd51e49b24 100644
--- a/usr/rsc/mem/malloc.h
+++ b/usr/rsc/mem/malloc.h
@@ -24,9 +24,13 @@ struct PageMap
void *level0[PMLevelSize];
};
+extern int64 allocator·allocated;\n
extern int64 allocator·footprint;\n
extern bool allocator·frozen;\n
void* trivalloc(int32);\n
void* pmlookup(PageMap*, uintptr);\n
void* pminsert(PageMap*, uintptr, void*);\n
+\n
+void*\talloc(int32);\n
+void\tfree(void*);\ndiff --git a/usr/rsc/mem/stack.c b/usr/rsc/mem/stack.c
new file mode 100644
index 0000000000..295e709ffb
--- /dev/null
+++ b/usr/rsc/mem/stack.c
@@ -0,0 +1,22 @@
+// Copyright 2009 The Go Authors. All rights reserved.\n
+// Use of this source code is governed by a BSD-style\n
+// license that can be found in the LICENSE file.\n
+\n
+#include "malloc.h"\n
+\n
+void*\n
+stackalloc(uint32 n)\n
+{\n
+\tvoid *v;\n
+\n
+\tv = alloc(n);\n
+//printf("stackalloc %d = %p\\n", n, v);\n
+\treturn v;\n
+}\n+\n
+void\n
+stackfree(void *v)\n
+{\n
+//printf("stackfree %p\\n", v);\n
+\tfree(v);\n
+}\ndiff --git a/usr/rsc/mem/triv.c b/usr/rsc/mem/triv.c
index 631e93a094..935cb9fc08 100644
--- a/usr/rsc/mem/triv.c
+++ b/usr/rsc/mem/triv.c
@@ -36,6 +36,7 @@ trivalloc(int32 size)
static byte *p;\n
static int32 n;\n
byte *v;\n
+\tuint64 oldfoot;\n
\n if(allocator·frozen)\n throw("allocator frozen");\n@@ -44,6 +45,7 @@ trivalloc(int32 size)
//sys·printint(size);\n //prints("\n");\n \n+\toldfoot = allocator·footprint;\n if(size < 4096) { // TODO: Tune constant.\n size = (size + Round) & ~Round;\n if(size > n) {\n@@ -53,12 +55,20 @@ trivalloc(int32 size)
}\n \tv = p;\n \tp += size;\n-\t\treturn v;\n+\t\tgoto out;\n \t}\n if(size & PageMask)\n \tsize += (1<<PageShift) - (size & PageMask);\n \tv = sys·mmap(nil, size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, 0, 0);\n \tallocator·footprint += size;\n+\n+out:\n+\tif((oldfoot>>24) != (allocator·footprint>>24))\n+\t\tprintf("memory footprint = %D MB for %D MB\\n", allocator·footprint>>20, allocator·allocated>>20);\n+\tif(allocator·footprint >= 2LL<<30) {\n+\t\tprints("out of memory\\n");\n+\t\tsys·exit(1);\n+\t}\n \treturn v;\n }\n \n```
### `usr/rsc/mem/allocator.go`
```diff
--- a/usr/rsc/mem/allocator.go
+++ b/usr/rsc/mem/allocator.go
@@ -10,3 +10,4 @@ export func memset(*byte, int, int)
export var footprint int64
export var frozen bool
export func testsizetoclass()
+export var allocated int64
コアとなるコードの解説
デバッグ出力の整理 (malloc.c
)
malloc.c
内の多くのprints
やsys·printint
呼び出しがコメントアウトまたはprintf
に置き換えられています。これは、Goランタイムの初期開発段階でデバッグのために挿入されたもので、詳細なログ出力は開発時には有用ですが、本番環境ではパフォーマンスのボトルネックとなり、メモリ使用量も増加させます。これらの変更は、デバッグ情報をより効率的なprintf
ベースの形式に移行するか、完全に削除することで、ランタイムのオーバーヘッドを削減し、メモリ効率を向上させることを目的としています。特に、prints
はGoの初期のデバッグ用プリミティブであり、より標準的なCのprintf
への移行は、コードの標準化と効率化の一環と考えられます。
allocator·allocated
の導入と追跡 (malloc.c
, allocator.go
, malloc.h
)
このコミットの最も重要な変更点の一つは、allocator·allocated
というグローバル変数の導入です。
malloc.h
でextern int64 allocator·allocated;
として宣言され、Cコード全体でアクセス可能になります。allocator.go
でexport var allocated int64
としてエクスポートされ、Goコードからもこの値にアクセスできるようになります。malloc.c
のalloc
関数内で、allocsmall
とalloclarge
の呼び出し前に、実際に割り当てられるメモリサイズ(classtosize[cl]
またはnp<<PageShift
)がallocator·allocated
に加算されます。free
関数内では、解放されるメモリサイズがallocator·allocated
から減算されます。
この変数は、Goランタイムが現在アプリケーションに割り当てているメモリの総量を正確に追跡するために使用されます。これにより、メモリ使用量のプロファイリング、デバッグ、および最適化が容易になります。例えば、メモリリークの検出や、特定の操作がどれだけのメモリを消費しているかの分析に役立ちます。
スタック割り当てロジックの分離 (malloc.c
からstack.c
へ)
以前malloc.c
内に存在したallocstack
とfreestack
関数が削除され、新しく作成されたstack.c
ファイルに移動されました。
stack.c
には、stackalloc
とstackfree
という新しい関数が定義され、それぞれ内部で汎用的なalloc
とfree
を呼び出しています。
この変更は、コードのモジュール性と関心事の分離を促進します。Goのゴルーチンは動的にスタックを拡張・縮小する能力を持っており、スタックの管理は一般的なヒープメモリの管理とは異なる特性を持つ場合があります。スタック関連のロジックを専用のファイルに分離することで、コードベースの整理が進み、将来的なスタック管理の最適化や変更が容易になります。
メモリフットプリントの監視とOOM検出 (triv.c
)
triv.c
のtrivalloc
関数に、メモリフットプリント(OSからプロセスに割り当てられた総メモリ量)を監視し、メモリ不足を検出するロジックが追加されました。
oldfoot
変数を導入し、allocator·footprint
の以前の値を保持します。allocator·footprint
が16MB(>>24
)の境界を越えるたびに、現在のメモリフットプリントとallocator·allocated
の値をMB単位で出力します。これは、メモリ使用量の傾向を把握するためのデバッグ出力です。allocator·footprint
が2GB(2LL<<30
)を超えた場合、「out of memory」メッセージを出力し、sys·exit(1)
でプログラムを終了します。これは、システム全体の安定性を保つための早期のメモリ不足検出メカニズムです。
これらの変更は、Goランタイムがメモリをより効率的に管理し、メモリ使用量に関するより良い可視性を提供するための基盤を築いています。特に、allocator·allocated
の導入は、Goのメモリプロファイリングツールやデバッグ機能の発展に不可欠なステップでした。
関連リンク
- Go言語のメモリ管理に関する公式ドキュメント(現在のバージョン): https://go.dev/doc/gc-guide
- Goの初期のメモリ管理に関する議論や設計ドキュメント(もし公開されていれば、このコミットの背景をより深く理解できる可能性がありますが、この特定のコミットに関する直接的な公開資料は見つかりませんでした。)
参考にした情報源リンク
このコミットに関する直接的な情報源は、Goの公式リポジトリのコミット履歴と、Go言語のメモリ管理に関する一般的な知識に基づいています。特定の外部記事やドキュメントを直接参照したわけではありません。