[インデックス 18188] ファイルの概要
このコミットは、Goランタイムにおけるファイナライザとヒーププロファイリング情報の管理方法を根本的に変更するものです。具体的には、これまでヒープビットマップに埋め込まれていた「特殊ビット」や、個別のハッシュテーブルで管理されていたこれらの情報を、MSpan
構造体に直接ぶら下がる「特殊レコード(special records)」のリンクリストとして管理するように変更します。これにより、オーバーヘッドの削減と、ヒープオブジェクトに付随するメタデータを管理するためのより汎用的なメカニズムが提供されます。
コミット
commit 020b39c3f3d3826d02c735c29d1dae7282aeb3f7
Author: Keith Randall <khr@golang.org>
Date: Tue Jan 7 13:45:50 2014 -0800
runtime: use special records hung off the MSpan to
record finalizers and heap profile info. Enables
removing the special bit from the heap bitmap. Also
provides a generic mechanism for annotating occasional
heap objects.
finalizers
overhead per obj
old 680 B 80 B avg
new 16 B/span 48 B
profile
overhead per obj
old 32KB 24 B + hash tables
new 16 B/span 24 B
R=cshapiro, khr, dvyukov, gobot
CC=golang-codereviews
https://golang.org/cl/13314053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/020b39c3f3d3826d02c735c29d1dae7282aeb3f7
元コミット内容
Goランタイムにおいて、ファイナライザとヒーププロファイリング情報を記録するために、MSpan
にぶら下がる特殊レコードを使用する。これにより、ヒープビットマップから特殊ビットを削除できるようになる。また、時折ヒープオブジェクトに注釈を付けるための汎用的なメカニズムも提供する。
ファイナライザのオーバーヘッド:
- 旧方式: 680 B (全体), 80 B (オブジェクトあたり平均)
- 新方式: 16 B/span (MSpanあたり), 48 B (オブジェクトあたり)
プロファイルのオーバーヘッド:
- 旧方式: 32KB (全体), 24 B + ハッシュテーブル (オブジェクトあたり)
- 新方式: 16 B/span (MSpanあたり), 24 B (オブジェクトあたり)
変更の背景
Goランタイムは、ガベージコレクション(GC)やプロファイリングのために、ヒープ上のオブジェクトに関するメタデータを管理する必要があります。このコミット以前は、ファイナライザやヒーププロファイリングの対象となるオブジェクトには、ヒープビットマップ内の特定のビット(bitSpecial
)が設定され、さらに詳細な情報は個別のグローバルなハッシュテーブル(mfinal.c
のファイナライザテーブル、mprof.goc
のプロファイル用アドレスバケット)で管理されていました。
この方式にはいくつかの課題がありました。
- オーバーヘッド: 特にヒーププロファイリングにおいては、グローバルなハッシュテーブルの管理と、それに伴うメモリ使用量(32KBのオーバーヘッド)が無視できないものでした。ファイナライザも同様に、オブジェクトあたりのオーバーヘッドがありました。
- 複雑性: ヒープビットマップの特殊ビットと、それに対応するグローバルなデータ構造の間で情報を同期させる必要があり、ランタイムのコードが複雑になっていました。
- 汎用性の欠如:
bitSpecial
はファイナライザとプロファイリングという特定の用途に限定されており、将来的に他の種類のメタデータをヒープオブジェクトに関連付けたい場合に、同様のメカニズムを再実装する必要がありました。
このコミットは、これらの課題を解決し、より効率的で汎用的なメタデータ管理メカニズムを導入することを目的としています。コミットメッセージに示されているように、新しい方式ではファイナライザとプロファイリングの両方でオーバーヘッドが大幅に削減されています。
前提知識の解説
このコミットを理解するためには、Goランタイムのメモリ管理とガベージコレクションに関する基本的な知識が必要です。
- MSpan: Goランタイムのメモリ管理において、
MSpan
は連続したページ(通常は8KB)のチャンクを表します。ヒープはこれらのMSpan
の集合で構成され、各MSpan
は特定のサイズのオブジェクト(スモールオブジェクト)を割り当てるために使用されるか、またはラージオブジェクト全体を保持します。MSpan
は、その中に含まれるオブジェクトのサイズクラスや状態などの情報を持っています。 - ヒープビットマップ: GoのGCは、ヒープ上のオブジェクトが参照されているかどうかを追跡するためにビットマップを使用します。各オブジェクトの先頭には、そのオブジェクトがマークされているか(GCの対象であるか)、スキャン済みか、特殊なプロパティを持つか(このコミット以前の
bitSpecial
)などの情報を示すビットが関連付けられています。 - ファイナライザ: Goの
runtime.SetFinalizer
関数を使用すると、オブジェクトがGCによって回収される直前に実行される関数を登録できます。これは、外部リソース(ファイルハンドル、ネットワーク接続など)をクリーンアップするのに役立ちます。 - ヒーププロファイリング: Goのプロファイリングツールは、プログラムがヒープメモリをどのように使用しているかを分析するのに役立ちます。これにより、メモリリークや非効率なメモリ使用を特定できます。ヒーププロファイリングは、どのコードパスがメモリを割り当てているかを追跡するために、割り当てられたオブジェクトに関するメタデータを記録します。
- FixAlloc: Goランタイム内で使用されるアロケータの一種で、固定サイズのオブジェクトを効率的に割り当てるために設計されています。これは、頻繁に割り当て・解放される小さなデータ構造(例:
MSpan
構造体自体や、このコミットで導入されるSpecial
レコード)の管理に適しています。
技術的詳細
このコミットの核心は、ファイナライザとヒーププロファイリングのメタデータを、MSpan
構造体自体に直接関連付ける新しいメカニズムの導入です。
-
Special
レコードの導入:runtime/malloc.h
にSpecial
という新しい構造体が導入されます。これは、MSpan
内のオブジェクトに付随する汎用的なメタデータを表すための基底構造体です。Special
構造体は、next
ポインタ(リンクリスト用)、offset
(MSpan
の開始からのオブジェクトのオフセット)、kind
(メタデータの種類、例: ファイナライザ、プロファイル)を持ちます。KindSpecialFinalizer
とKindSpecialProfile
という列挙型が定義され、それぞれファイナライザとプロファイリングのための特殊レコードの種類を示します。SpecialFinalizer
とSpecialProfile
という具体的な特殊レコード構造体が定義され、それぞれファイナライザ関数やプロファイルバケットなどの特定のメタデータを保持します。
-
MSpan
へのspecials
リンクリストの追加:MSpan
構造体にSpecial *specials;
というフィールドが追加されます。これは、そのMSpan
が管理するメモリブロックに設定されたすべての特殊レコードのリンクリストのヘッドとなります。MSpan
にはLock specialLock;
も追加され、specials
リンクリストへのアクセスを保護します。
-
ファイナライザ管理の変更:
src/pkg/runtime/mfinal.c
ファイルが完全に削除されます。これは、これまでファイナライザを管理していたグローバルなハッシュテーブルベースのシステムが廃止されたことを意味します。runtime.SetFinalizer
は、新しいruntime·addfinalizer
関数とruntime·removefinalizer
関数を使用するように変更されます。これらの関数は、オブジェクトに対応するSpecialFinalizer
レコードを作成し、それを適切なMSpan
のspecials
リンクリストに追加/削除します。- GCのルートスキャン時(
mgc0.c
のaddroots
関数)に、すべてのMSpan
のspecials
リンクリストが走査され、KindSpecialFinalizer
のレコードが見つかった場合、そのファイナライズ対象オブジェクトがGCルートとして扱われます。これにより、ファイナライザが設定されたオブジェクトは、ファイナライザが実行されるまで回収されなくなります。 - オブジェクトが解放される際(
mgc0.c
のsweepspan
関数)、そのオブジェクトに関連付けられた特殊レコードがspecials
リンクリストから削除されます。もしそれがファイナライザレコードであれば、runtime·queuefinalizer
を呼び出してファイナライザキューに追加し、オブジェクトの実際の解放はファイナライザの実行後に行われるようにします。
-
ヒーププロファイリング管理の変更:
src/pkg/runtime/mprof.goc
から、アドレスバケット(AddrHash
,AddrEntry
)を管理する複雑なハッシュテーブルのコードが削除されます。runtime·MProf_Malloc
は、割り当てられたオブジェクトに対してruntime·setprofilebucket
を呼び出すように変更されます。この関数は、SpecialProfile
レコードを作成し、それをMSpan
のspecials
リンクリストに追加します。runtime·MProf_Free
は、解放されるオブジェクトに関連付けられたSpecialProfile
レコードから直接プロファイルバケット情報を受け取るように変更されます。これにより、解放時に再度アドレスからバケットを検索する必要がなくなります。
-
オーバーヘッドの削減:
- コミットメッセージに示されているように、この変更によりファイナライザとプロファイリングの両方でオーバーヘッドが大幅に削減されます。これは、グローバルなハッシュテーブルの代わりに、
MSpan
ごとに局所的なリンクリストを使用することで、メモリの局所性が向上し、ロックの競合が減少するためです。特に、プロファイリングの32KBのオーバーヘッドが16B/spanに削減されるのは顕著です。
- コミットメッセージに示されているように、この変更によりファイナライザとプロファイリングの両方でオーバーヘッドが大幅に削減されます。これは、グローバルなハッシュテーブルの代わりに、
-
汎用的なメカニズム:
Special
レコードの導入により、Goランタイムは将来的に他の種類のメタデータをヒープオブジェクトに関連付けるための汎用的なフレームワークを手に入れました。新しい種類のメタデータを追加したい場合、新しいKindSpecialXxx
とSpecialXxx
構造体を定義し、MSpan
のspecials
リンクリストに追加するロジックを実装するだけでよくなります。
コアとなるコードの変更箇所
このコミットは広範囲にわたる変更を含みますが、特に重要な変更箇所は以下のファイルに集中しています。
src/pkg/runtime/malloc.h
:Special
、SpecialFinalizer
、SpecialProfile
構造体の定義、MSpan
構造体へのspecials
フィールドとspecialLock
フィールドの追加、および関連する新しい関数のプロトタイプ宣言。src/pkg/runtime/mfinal.c
: ファイル全体が削除。旧ファイナライザ管理システムの廃止。src/pkg/runtime/mheap.c
:addspecial
、removespecial
、runtime·addfinalizer
、runtime·removefinalizer
、runtime·setprofilebucket
、runtime·freespecial
、runtime·freeallspecials
といった、Special
レコードのライフサイクルとMSpan
のspecials
リンクリストの管理を行う関数の実装。src/pkg/runtime/mgc0.c
: GCのルートスキャン(addroots
)とスイープ(sweepspan
)のロジックが変更され、MSpan.specials
リンクリストを走査してファイナライザやプロファイル情報を処理するように修正。src/pkg/runtime/mprof.goc
: 旧プロファイリング用アドレスバケット管理コードの削除と、新しいSpecialProfile
レコードを使用するruntime·MProf_Malloc
およびruntime·MProf_Free
の実装。src/pkg/runtime/malloc.goc
:runtime.SetFinalizer
の実装が新しいaddfinalizer
/removefinalizer
関数を使用するように変更され、runtime·MProf_Malloc
とruntime·free
がSpecial
レコードのメカニズムを利用するように修正。
コアとなるコードの解説
src/pkg/runtime/malloc.h
// 新しい特殊レコードの種類
enum
{
KindSpecialFinalizer = 1,
KindSpecialProfile = 2,
// Note: The finalizer special must be first because if we're freeing
// an object, a finalizer special will cause the freeing operation
// to abort, and we want to keep the other special records around
// if that happens.
};
// 特殊レコードの基底構造体
typedef struct Special Special;
struct Special
{
Special* next; // span内のリンクリストの次の要素
uint16 offset; // spanの開始からのオブジェクトのオフセット
byte kind; // Specialの種類
};
// ファイナライザのための特殊レコード
typedef struct SpecialFinalizer SpecialFinalizer;
struct SpecialFinalizer
{
Special; // Special構造体を埋め込み
FuncVal* fn; // ファイナライザ関数
uintptr nret; // 戻り値のサイズ
Type* fint; // ファイナライザ関数の型情報
PtrType* ot; // オブジェクトのポインタ型
};
// ヒーププロファイリングのための特殊レコード
typedef struct Bucket Bucket; // mprof.gocから
typedef struct SpecialProfile SpecialProfile;
struct SpecialProfile
{
Special; // Special構造体を埋め込み
Bucket* b; // プロファイルバケット
};
// MSpan構造体への変更
struct MSpan
{
// ... 既存のフィールド ...
Lock specialLock; // specialsリンクリストを保護するためのロック
Special *specials; // 特殊レコードのリンクリスト(オフセット順にソートされる)
};
// MHeap構造体への変更
struct MHeap
{
// ... 既存のフィールド ...
FixAlloc specialfinalizeralloc; // SpecialFinalizer* のためのアロケータ
FixAlloc specialprofilealloc; // SpecialProfile* のためのアロケータ
Lock speciallock; // 特殊レコードアロケータのためのロック
};
MSpan
にspecials
というリンクリストが追加されたことが最も重要な変更点です。これにより、特定のMSpan
に属するすべての特殊なオブジェクト(ファイナライザを持つオブジェクトやプロファイリング対象のオブジェクト)のメタデータが、そのMSpan
から直接アクセスできるようになります。
src/pkg/runtime/mheap.c
// オブジェクトpに特殊レコードsを追加する
static bool
addspecial(void *p, Special *s)
{
MSpan *span;
Special **t, *x;
uintptr offset;
byte kind;
span = runtime·MHeap_LookupMaybe(&runtime·mheap, p); // pが属するMSpanを検索
if(span == nil)
runtime·throw("addspecial on invalid pointer");
offset = (uintptr)p - (span->start << PageShift); // span開始からのオフセットを計算
kind = s->kind;
runtime·lock(&span->specialLock); // MSpanのロックを取得
// 挿入ポイントを見つけ、既存のレコードがないかチェック
t = &span->specials;
while((x = *t) != nil) {
if(offset == x->offset && kind == x->kind) {
runtime·unlock(&span->specialLock);
return false; // 既に存在する
}
// オフセットと種類でソートされたリンクリストに挿入
if(offset < x->offset || (offset == x->offset && kind < x->kind))
break;
t = &x->next;
}
// レコードを挿入し、オフセットを設定
s->offset = offset;
s->next = x;
*t = s;
runtime·unlock(&span->specialLock); // ロックを解放
return true;
}
// オブジェクトpから指定された種類の特殊レコードを削除する
static Special*
removespecial(void *p, byte kind)
{
MSpan *span;
Special *s, **t;
uintptr offset;
span = runtime·MHeap_LookupMaybe(&runtime·mheap, p);
if(span == nil)
runtime·throw("removespecial on invalid pointer");
offset = (uintptr)p - (span->start << PageShift);
runtime·lock(&span->specialLock);
t = &span->specials;
while((s = *t) != nil) {
if(offset == s->offset && kind == s->kind) {
*t = s->next; // リンクリストから削除
runtime·unlock(&span->specialLock);
return s; // 削除されたレコードを返す
}
t = &s->next;
}
runtime·unlock(&span->specialLock);
return nil;
}
// オブジェクトpにファイナライザを追加する
bool
runtime·addfinalizer(void *p, FuncVal *f, uintptr nret, Type *fint, PtrType *ot)
{
SpecialFinalizer *s;
runtime·lock(&runtime·mheap.speciallock); // アロケータのロック
s = runtime·FixAlloc_Alloc(&runtime·mheap.specialfinalizeralloc); // SpecialFinalizerを割り当て
runtime·unlock(&runtime·mheap.speciallock);
s->kind = KindSpecialFinalizer;
s->fn = f;
s->nret = nret;
s->fint = fint;
s->ot = ot;
if(addspecial(p, s)) // MSpanのリンクリストに追加
return true;
// 既存のファイナライザがあった場合
runtime·lock(&runtime·mheap.speciallock);
runtime·FixAlloc_Free(&runtime·mheap.specialfinalizeralloc, s); // 割り当てたSpecialFinalizerを解放
runtime·unlock(&runtime·mheap.speciallock);
return false;
}
// オブジェクトpからファイナライザを削除する
void
runtime·removefinalizer(void *p)
{
SpecialFinalizer *s;
s = (SpecialFinalizer*)removespecial(p, KindSpecialFinalizer); // レコードを削除
if(s == nil)
return; // 削除するファイナライザがなかった
runtime·lock(&runtime·mheap.speciallock);
runtime·FixAlloc_Free(&runtime·mheap.specialfinalizeralloc, s); // 削除したレコードを解放
runtime·unlock(&runtime·mheap.speciallock);
}
// オブジェクトpに関連付けられたプロファイルバケットを設定する
void
runtime·setprofilebucket(void *p, Bucket *b)
{
SpecialProfile *s;
runtime·lock(&runtime·mheap.speciallock);
s = runtime·FixAlloc_Alloc(&runtime·mheap.specialprofilealloc);
runtime·unlock(&runtime·mheap.speciallock);
s->kind = KindSpecialProfile;
s->b = b;
if(!addspecial(p, s))
runtime·throw("setprofilebucket: profile already set");
}
// 特殊レコードsを解放する(MSpanのリンクリストから既に削除されている)
// pはオブジェクトのポインタ、sizeはオブジェクトのサイズ
// オブジェクトpの解放を継続すべきかどうかを返す
bool
runtime·freespecial(Special *s, void *p, uintptr size)
{
SpecialFinalizer *sf;
SpecialProfile *sp;
switch(s->kind) {
case KindSpecialFinalizer:
sf = (SpecialFinalizer*)s;
runtime·queuefinalizer(p, sf->fn, sf->nret, sf->fint, sf->ot); // ファイナライザキューに追加
runtime·lock(&runtime·mheap.speciallock);
runtime·FixAlloc_Free(&runtime·mheap.specialfinalizeralloc, sf);
runtime·unlock(&runtime·mheap.speciallock);
return false; // ファイナライザが完了するまでpを解放しない
case KindSpecialProfile:
sp = (SpecialProfile*)s;
runtime·MProf_Free(sp->b, p, size); // プロファイル情報を更新
runtime·lock(&runtime·mheap.speciallock);
runtime·FixAlloc_Free(&runtime·mheap.specialprofilealloc, sp);
runtime·unlock(&runtime·mheap.speciallock);
return true; // pの解放を継続
default:
runtime·throw("bad special kind");
return true;
}
}
// オブジェクトpに関連付けられたすべての特殊レコードを解放する
void
runtime·freeallspecials(MSpan *span, void *p, uintptr size)
{
Special *s, **t;
uintptr offset;
offset = (uintptr)p - (span->start << PageShift);
runtime·lock(&span->specialLock);
t = &span->specials;
while((s = *t) != nil) {
if(offset < s->offset)
break;
if(offset == s->offset) {
*t = s->next; // リンクリストから削除
if(!runtime·freespecial(s, p, size)) // レコードを解放し、オブジェクトの解放を継続するか判断
runtime·throw("can't explicitly free an object with a finalizer");
} else
t = &s->next;
}
runtime·unlock(&span->specialLock);
}
addspecial
とremovespecial
は、MSpan
のspecials
リンクリストを管理する汎用的なヘルパー関数です。runtime·addfinalizer
、runtime·removefinalizer
、runtime·setprofilebucket
は、これらのヘルパー関数を使って特定の種類の特殊レコードを管理します。runtime·freespecial
は、特殊レコードが解放される際に、その種類に応じて適切なクリーンアップ処理(ファイナライザのキューイングやプロファイル情報の更新)を行う重要な関数です。
src/pkg/runtime/mgc0.c
static void
addroots(void)
{
// ... 既存のルート追加ロジック ...
// MSpan.specials を走査してルートを追加
allspans = runtime·mheap.allspans;
for(spanidx=0; spanidx<runtime·mheap.nspan; spanidx++) {
s = allspans[spanidx];
if(s->state != MSpanInUse)
continue;
for(sp = s->specials; sp != nil; sp = sp->next) {
switch(sp->kind) {
case KindSpecialFinalizer:
spf = (SpecialFinalizer*)sp;
// ファイナライズ対象オブジェクト自体はマークしないが、
// それが指すものはスキャンして保持する
addroot((Obj){(void*)((s->start << PageShift) + spf->offset), s->elemsize, 0});
// ファイナライザ関数、型情報などもルートとして追加
addroot((Obj){(void*)&spf->fn, PtrSize, 0});
addroot((Obj){(void*)&spf->fint, PtrSize, 0});
addroot((Obj){(void*)&spf->ot, PtrSize, 0});
break;
case KindSpecialProfile:
// プロファイル情報はGCルートではないので何もしない
break;
}
}
}
// ... 既存のルート追加ロジック ...
}
static void
sweepspan(ParFor *desc, uint32 idx)
{
// ... 既存のスイープロジック ...
// 解放されようとしているオブジェクトの特殊レコードをリンクリストから削除し、解放する
specialp = &s->specials;
special = *specialp;
while(special != nil) {
p = (byte*)(s->start << PageShift) + special->offset;
off = (uintptr*)p - (uintptr*)arena_start;
bitp = (uintptr*)arena_start - off/wordsPerBitmapWord - 1;
shift = off % wordsPerBitmapWord;
bits = *bitp>>shift;
if((bits & (bitAllocated|bitMarked)) == bitAllocated) {
// オブジェクトが解放されようとしている場合: 特殊レコードをリンクリストから削除
y = special;
special = special->next;
*specialp = special;
if(!runtime·freespecial(y, p, size)) {
// ファイナライザを持つオブジェクトの場合、解放を中止
*bitp |= bitMarked << shift; // オブジェクトをマーク済みとして扱う
}
} else {
// オブジェクトがまだ生きている場合: 特殊レコードを保持
specialp = &special->next;
special = *specialp;
}
}
// ... 既存のスイープロジック ...
}
addroots
では、GCがMSpan
のspecials
リンクリストを走査し、ファイナライザを持つオブジェクトをGCルートとしてマークすることで、ファイナライザが実行されるまでオブジェクトが回収されないようにします。sweepspan
では、GCがオブジェクトをスイープする際に、そのオブジェクトに関連付けられた特殊レコードを処理し、ファイナライザを持つオブジェクトの解放を適切に遅延させます。
関連リンク
- Go言語のガベージコレクションに関するドキュメントやブログ記事
- Go言語のプロファイリングに関するドキュメント
参考にした情報源リンク
- Goのソースコード(特に
src/pkg/runtime/
ディレクトリ) - Goのコミット履歴
- GoのGerritコードレビューシステム (https://golang.org/cl/13314053) - (直接アクセスはできませんでしたが、コミットメッセージから参照されています)
- Goのメモリ管理に関する技術記事や論文