Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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のプロファイル用アドレスバケット)で管理されていました。

この方式にはいくつかの課題がありました。

  1. オーバーヘッド: 特にヒーププロファイリングにおいては、グローバルなハッシュテーブルの管理と、それに伴うメモリ使用量(32KBのオーバーヘッド)が無視できないものでした。ファイナライザも同様に、オブジェクトあたりのオーバーヘッドがありました。
  2. 複雑性: ヒープビットマップの特殊ビットと、それに対応するグローバルなデータ構造の間で情報を同期させる必要があり、ランタイムのコードが複雑になっていました。
  3. 汎用性の欠如: bitSpecialはファイナライザとプロファイリングという特定の用途に限定されており、将来的に他の種類のメタデータをヒープオブジェクトに関連付けたい場合に、同様のメカニズムを再実装する必要がありました。

このコミットは、これらの課題を解決し、より効率的で汎用的なメタデータ管理メカニズムを導入することを目的としています。コミットメッセージに示されているように、新しい方式ではファイナライザとプロファイリングの両方でオーバーヘッドが大幅に削減されています。

前提知識の解説

このコミットを理解するためには、Goランタイムのメモリ管理とガベージコレクションに関する基本的な知識が必要です。

  • MSpan: Goランタイムのメモリ管理において、MSpanは連続したページ(通常は8KB)のチャンクを表します。ヒープはこれらのMSpanの集合で構成され、各MSpanは特定のサイズのオブジェクト(スモールオブジェクト)を割り当てるために使用されるか、またはラージオブジェクト全体を保持します。MSpanは、その中に含まれるオブジェクトのサイズクラスや状態などの情報を持っています。
  • ヒープビットマップ: GoのGCは、ヒープ上のオブジェクトが参照されているかどうかを追跡するためにビットマップを使用します。各オブジェクトの先頭には、そのオブジェクトがマークされているか(GCの対象であるか)、スキャン済みか、特殊なプロパティを持つか(このコミット以前のbitSpecial)などの情報を示すビットが関連付けられています。
  • ファイナライザ: Goのruntime.SetFinalizer関数を使用すると、オブジェクトがGCによって回収される直前に実行される関数を登録できます。これは、外部リソース(ファイルハンドル、ネットワーク接続など)をクリーンアップするのに役立ちます。
  • ヒーププロファイリング: Goのプロファイリングツールは、プログラムがヒープメモリをどのように使用しているかを分析するのに役立ちます。これにより、メモリリークや非効率なメモリ使用を特定できます。ヒーププロファイリングは、どのコードパスがメモリを割り当てているかを追跡するために、割り当てられたオブジェクトに関するメタデータを記録します。
  • FixAlloc: Goランタイム内で使用されるアロケータの一種で、固定サイズのオブジェクトを効率的に割り当てるために設計されています。これは、頻繁に割り当て・解放される小さなデータ構造(例: MSpan構造体自体や、このコミットで導入されるSpecialレコード)の管理に適しています。

技術的詳細

このコミットの核心は、ファイナライザとヒーププロファイリングのメタデータを、MSpan構造体自体に直接関連付ける新しいメカニズムの導入です。

  1. Specialレコードの導入:

    • runtime/malloc.hSpecialという新しい構造体が導入されます。これは、MSpan内のオブジェクトに付随する汎用的なメタデータを表すための基底構造体です。
    • Special構造体は、nextポインタ(リンクリスト用)、offsetMSpanの開始からのオブジェクトのオフセット)、kind(メタデータの種類、例: ファイナライザ、プロファイル)を持ちます。
    • KindSpecialFinalizerKindSpecialProfileという列挙型が定義され、それぞれファイナライザとプロファイリングのための特殊レコードの種類を示します。
    • SpecialFinalizerSpecialProfileという具体的な特殊レコード構造体が定義され、それぞれファイナライザ関数やプロファイルバケットなどの特定のメタデータを保持します。
  2. MSpanへのspecialsリンクリストの追加:

    • MSpan構造体にSpecial *specials;というフィールドが追加されます。これは、そのMSpanが管理するメモリブロックに設定されたすべての特殊レコードのリンクリストのヘッドとなります。
    • MSpanにはLock specialLock;も追加され、specialsリンクリストへのアクセスを保護します。
  3. ファイナライザ管理の変更:

    • src/pkg/runtime/mfinal.cファイルが完全に削除されます。これは、これまでファイナライザを管理していたグローバルなハッシュテーブルベースのシステムが廃止されたことを意味します。
    • runtime.SetFinalizerは、新しいruntime·addfinalizer関数とruntime·removefinalizer関数を使用するように変更されます。これらの関数は、オブジェクトに対応するSpecialFinalizerレコードを作成し、それを適切なMSpanspecialsリンクリストに追加/削除します。
    • GCのルートスキャン時(mgc0.caddroots関数)に、すべてのMSpanspecialsリンクリストが走査され、KindSpecialFinalizerのレコードが見つかった場合、そのファイナライズ対象オブジェクトがGCルートとして扱われます。これにより、ファイナライザが設定されたオブジェクトは、ファイナライザが実行されるまで回収されなくなります。
    • オブジェクトが解放される際(mgc0.csweepspan関数)、そのオブジェクトに関連付けられた特殊レコードがspecialsリンクリストから削除されます。もしそれがファイナライザレコードであれば、runtime·queuefinalizerを呼び出してファイナライザキューに追加し、オブジェクトの実際の解放はファイナライザの実行後に行われるようにします。
  4. ヒーププロファイリング管理の変更:

    • src/pkg/runtime/mprof.gocから、アドレスバケット(AddrHash, AddrEntry)を管理する複雑なハッシュテーブルのコードが削除されます。
    • runtime·MProf_Mallocは、割り当てられたオブジェクトに対してruntime·setprofilebucketを呼び出すように変更されます。この関数は、SpecialProfileレコードを作成し、それをMSpanspecialsリンクリストに追加します。
    • runtime·MProf_Freeは、解放されるオブジェクトに関連付けられたSpecialProfileレコードから直接プロファイルバケット情報を受け取るように変更されます。これにより、解放時に再度アドレスからバケットを検索する必要がなくなります。
  5. オーバーヘッドの削減:

    • コミットメッセージに示されているように、この変更によりファイナライザとプロファイリングの両方でオーバーヘッドが大幅に削減されます。これは、グローバルなハッシュテーブルの代わりに、MSpanごとに局所的なリンクリストを使用することで、メモリの局所性が向上し、ロックの競合が減少するためです。特に、プロファイリングの32KBのオーバーヘッドが16B/spanに削減されるのは顕著です。
  6. 汎用的なメカニズム:

    • Specialレコードの導入により、Goランタイムは将来的に他の種類のメタデータをヒープオブジェクトに関連付けるための汎用的なフレームワークを手に入れました。新しい種類のメタデータを追加したい場合、新しいKindSpecialXxxSpecialXxx構造体を定義し、MSpanspecialsリンクリストに追加するロジックを実装するだけでよくなります。

コアとなるコードの変更箇所

このコミットは広範囲にわたる変更を含みますが、特に重要な変更箇所は以下のファイルに集中しています。

  • src/pkg/runtime/malloc.h: SpecialSpecialFinalizerSpecialProfile構造体の定義、MSpan構造体へのspecialsフィールドとspecialLockフィールドの追加、および関連する新しい関数のプロトタイプ宣言。
  • src/pkg/runtime/mfinal.c: ファイル全体が削除。旧ファイナライザ管理システムの廃止。
  • src/pkg/runtime/mheap.c: addspecialremovespecialruntime·addfinalizerruntime·removefinalizerruntime·setprofilebucketruntime·freespecialruntime·freeallspecialsといった、SpecialレコードのライフサイクルとMSpanspecialsリンクリストの管理を行う関数の実装。
  • 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_Mallocruntime·freeSpecialレコードのメカニズムを利用するように修正。

コアとなるコードの解説

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; // 特殊レコードアロケータのためのロック
};

MSpanspecialsというリンクリストが追加されたことが最も重要な変更点です。これにより、特定の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);
}

addspecialremovespecialは、MSpanspecialsリンクリストを管理する汎用的なヘルパー関数です。runtime·addfinalizerruntime·removefinalizerruntime·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がMSpanspecialsリンクリストを走査し、ファイナライザを持つオブジェクトをGCルートとしてマークすることで、ファイナライザが実行されるまでオブジェクトが回収されないようにします。sweepspanでは、GCがオブジェクトをスイープする際に、そのオブジェクトに関連付けられた特殊レコードを処理し、ファイナライザを持つオブジェクトの解放を適切に遅延させます。

関連リンク

  • Go言語のガベージコレクションに関するドキュメントやブログ記事
  • Go言語のプロファイリングに関するドキュメント

参考にした情報源リンク

  • Goのソースコード(特にsrc/pkg/runtime/ディレクトリ)
  • Goのコミット履歴
  • GoのGerritコードレビューシステム (https://golang.org/cl/13314053) - (直接アクセスはできませんでしたが、コミットメッセージから参照されています)
  • Goのメモリ管理に関する技術記事や論文