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

[インデックス 18668] ファイルの概要

このコミットは、Goランタイムのメモリ管理における重要な変更を導入しています。特に、settypeバッファとそれに関連するロックを排除し、MCache(各P(プロセッサ)ごとのキャッシュ)が特定のMSpan(メモリページの連続したブロック)に対して排他的なアクセスを持つようにすることで、並行処理のパフォーマンスを大幅に向上させています。これにより、マルチプロセッサ環境でのアロケーションベンチマークにおいて顕著な速度改善が見られます。

コミット

commit 3b5278fca624e802ae71351626719ba262d0c5d1
Author: Keith Randall <khr@golang.org>
Date:   Wed Feb 26 15:52:58 2014 -0800

    runtime: get rid of the settype buffer and lock.
    
    MCaches now hold a MSpan for each sizeclass which they have
    exclusive access to allocate from, so no lock is needed.
    
    Modifying the heap bitmaps also no longer requires a cas.
    
    runtime.free gets more expensive.  But we don't use it
    much any more.
    
    It's not much faster on 1 processor, but it's a lot
    faster on multiple processors.
    
    benchmark                 old ns/op    new ns/op    delta
    BenchmarkSetTypeNoPtr1           24           23   -0.42%
    BenchmarkSetTypeNoPtr2           33           34   +0.89%
    BenchmarkSetTypePtr1             51           49   -3.72%
    BenchmarkSetTypePtr2             55           54   -1.98%
    
    benchmark                old ns/op    new ns/op    delta
    BenchmarkAllocation          52739        50770   -3.73%
    BenchmarkAllocation-2        33957        34141   +0.54%
    BenchmarkAllocation-3        33326        29015  -12.94%
    BenchmarkAllocation-4        38105        25795  -32.31%
    BenchmarkAllocation-5        68055        24409  -64.13%
    BenchmarkAllocation-6        71544        23488  -67.17%
    BenchmarkAllocation-7        68374        23041  -66.30%
    BenchmarkAllocation-8        70117        20758  -70.40%
    
    LGTM=rsc, dvyukov
    R=dvyukov, bradfitz, khr, rsc
    CC=golang-codereviews
    https://golang.org/cl/46810043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3b5278fca624e802ae71351626719ba262d0c5d1

元コミット内容

runtime: get rid of the settype buffer and lock.

MCaches now hold a MSpan for each sizeclass which they have
exclusive access to allocate from, so no lock is needed.

Modifying the heap bitmaps also no longer requires a cas.

runtime.free gets more expensive.  But we don't use it
much any more.

It's not much faster on 1 processor, but it's a lot
faster on multiple processors.

benchmark                 old ns/op    new ns/op    delta
BenchmarkSetTypeNoPtr1           24           23   -0.42%
BenchmarkSetTypeNoPtr2           33           34   +0.89%
BenchmarkSetTypePtr1             51           49   -3.72%
BenchmarkSetTypePtr2             55           54   -1.98%

benchmark                old ns/op    new ns/op    delta
BenchmarkAllocation          52739        50770   -3.73%
BenchmarkAllocation-2        33957        34141   +0.54%
BenchmarkAllocation-3        33326        29015  -12.94%
BenchmarkAllocation-4        38105        25795  -32.31%
BenchmarkAllocation-5        68055        24409  -64.13%
BenchmarkAllocation-6        71544        23488  -67.17%
BenchmarkAllocation-7        68374        23041  -66.30%
BenchmarkAllocation-8        70117        20758  -70.40%

LGTM=rsc, dvyukov
R=dvyukov, bradfitz, khr, rsc
CC=golang-codereviews
https://golang.org/cl/46810043

変更の背景

このコミットの主な目的は、Goランタイムのメモリ割り当てにおける並行処理の効率を向上させることです。特に、マルチコアプロセッサ環境でのパフォーマンスボトルネックとなっていたsettypeバッファとそれに関連するロックの競合を解消することに焦点を当てています。

以前のGoランタイムでは、オブジェクトの型情報をヒープに記録する際に、settype_bufというバッファとsettype_lockというグローバルロックを使用していました。これは、複数のゴルーチンが同時にメモリを割り当てる際に、このロックを巡って競合が発生し、並行処理の効率を低下させる原因となっていました。

また、ヒープのビットマップ(メモリのどの部分が割り当てられているか、ポインタが含まれているかなどを追跡するためのデータ構造)の変更も、アトミック操作(CAS: Compare-And-Swap)を必要としており、これもまた並行処理のオーバーヘッドとなっていました。

このコミットは、これらの競合ポイントを排除することで、特に並行性の高いワークロードにおいて、メモリ割り当てのパフォーマンスを劇的に改善することを目指しています。ベンチマーク結果が示すように、特にプロセッサ数が増えるにつれて、BenchmarkAllocationの性能が大幅に向上しています。

前提知識の解説

このコミットを理解するためには、Goランタイムのメモリ管理の基本的な概念と、ガベージコレクション(GC)の仕組みについて理解しておく必要があります。

  • Goのメモリ管理の階層構造:

    • MHeap: Goランタイム全体のヒープを管理する最上位の構造体です。ページ(通常4KB)単位でメモリを管理します。
    • MSpan: 連続したメモリページのブロックを表します。MHeapから取得され、特定のサイズのオブジェクトを割り当てるために使用されます。各MSpanは、そのMSpanから割り当てられたオブジェクトの型情報(ポインタの有無など)を追跡します。
    • MCentral: 特定のサイズクラス(オブジェクトのサイズに応じた分類)のMSpanを管理する共有フリーリストです。MCacheがMSpanを要求する際に、MCentralから取得します。
    • MCache: 各P(プロセッサ、Goスケジューラがゴルーチンを実行する論理的なコンテキスト)に紐付けられたローカルなキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。MCacheは、MCentralからMSpanを取得し、そこからオブジェクトを割り当てます。これにより、グローバルなロックの競合を減らします。
    • P (Processor): Goランタイムのスケジューラがゴルーチンを実行するための論理的なプロセッサです。各Pは独自のMCacheを持ちます。
    • M (Machine): OSのスレッドを表します。MはPと関連付けられ、P上でゴルーチンを実行します。
  • ガベージコレクション (GC): Goはトレース型ガベージコレクタを使用しており、到達可能なオブジェクトを特定し、到達不能なオブジェクトを解放します。GCはヒープ上のオブジェクトの型情報を利用して、ポインタを正確にスキャンし、到達可能なオブジェクトをマークします。

  • ヒープビットマップ: Goランタイムは、ヒープ上のメモリブロックに関するメタデータをビットマップとして管理しています。これには、メモリブロックが割り当てられているか、ポインタが含まれているか、GCによってマークされているかなどの情報が含まれます。これらのビットマップは、GCの効率的な動作に不可欠です。

  • settype: Goランタイムがヒープにオブジェクトを割り当てる際に、そのオブジェクトの型情報をMSpanに記録する処理です。この型情報は、GCがヒープをスキャンする際に、どの部分がポインタであり、どの部分がそうでないかを判断するために使用されます。

  • CAS (Compare-And-Swap): アトミック操作の一種で、メモリ位置の現在の値が期待される値と等しい場合にのみ、そのメモリ位置を新しい値に更新します。マルチスレッド環境での競合を避けるために使用されますが、頻繁な使用はオーバーヘッドとなる可能性があります。

技術的詳細

このコミットの技術的な核心は、Goランタイムのメモリ割り当てパスからグローバルなロックとアトミック操作を排除し、並行性を高めることにあります。

  1. settypeバッファとロックの排除:

    • 以前は、M構造体(OSスレッドを表す)内にsettype_bufというバッファと、それを保護するためのグローバルなsettype_lockが存在しました。オブジェクトが割り当てられるたびに、その型情報がこのバッファに一時的に格納され、バッファが満杯になるとsettype_flush関数がロックを取得してバッファの内容を処理していました。
    • このコミットでは、settype_bufsettype_lockが完全に削除されました。代わりに、settype関数は、オブジェクトが割り当てられた直後に、そのオブジェクトが属するMSpanに直接型情報を記録するように変更されました。
    • この変更の鍵は、MCacheが特定のsizeclass(オブジェクトのサイズカテゴリ)に対応するMSpanを排他的に所有するようになったことです。これにより、settype関数がMSpanの型情報を更新する際に、他のゴルーチンとの競合を心配する必要がなくなり、ロックが不要になりました。
  2. ヒープビットマップの変更におけるCASの排除:

    • 以前は、markscan(GCがオブジェクトをスキャンする際にビットマップを更新する)やmarkfreed(オブジェクトが解放された際にビットマップを更新する)のような操作で、ヒープビットマップを変更する際にCAS操作を使用していました。これは、複数のゴルーチンが同時にビットマップを更新しようとする際の競合を避けるためでした。
    • このコミットでは、markscanmarkfreed関数内のCASループが削除され、直接的なビットマップの書き込みに置き換えられました。これは、MCacheMSpanを排他的に所有するようになったことで、ビットマップの変更がローカルな操作となり、競合の可能性が大幅に減少したためです。
  3. MCacheMSpanの関連性の強化:

    • MCache構造体は、MCacheList list[NumSizeClasses]からMSpan* alloc[NumSizeClasses]MCacheList free[NumSizeClasses]に変更されました。
    • alloc[sizeclass]は、そのMCacheが現在割り当てを行っているMSpanへのポインタを保持します。このMSpanは、そのMCacheによって排他的に所有されます。
    • free[sizeclass]は、明示的に解放されたオブジェクトのリストを保持します。これらのオブジェクトは、後でMCentralに返されるか、同じMCacheによって再利用されます。
    • MCache_Refill関数は、MCentralから新しいMSpanを取得し、それをc->alloc[sizeclass]に設定するように変更されました。
    • MCache_Free関数は、オブジェクトをc->free[sizeclass]リストに追加するように変更されました。もし、そのオブジェクトが現在MCacheが所有しているMSpanに属している場合は、そのMSpanのフリーリストに直接追加されます。
  4. runtime.freeのコスト増加:

    • コミットメッセージには「runtime.free gets more expensive. But we don't use it much any more.」とあります。これは、オブジェクトの解放パスが以前よりも複雑になったことを示唆しています。特に、MCache_Freeのロジックが変更され、オブジェクトがMCacheが所有するMSpanに属しているかどうかをチェックし、そうでない場合はフリーキューに追加するなどの処理が加わったためと考えられます。しかし、Goのランタイムでは、GCがほとんどのメモリ解放を処理するため、runtime.freeが直接呼び出される頻度は低く、このコスト増加は全体的なパフォーマンスに大きな影響を与えないと判断されています。

これらの変更により、Goランタイムのメモリ割り当ては、特にマルチコア環境において、よりスケーラブルで効率的になりました。

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

このコミットでは、主に以下のファイルが変更されています。

  • src/pkg/runtime/gc_test.go: 新しいベンチマークが追加されました。BenchmarkSetTypeNoPtrX, BenchmarkSetTypePtrX, BenchmarkAllocationなど、メモリ割り当てと型設定のパフォーマンスを測定するためのテストです。
  • src/pkg/runtime/malloc.goc: メモリ割り当ての主要なロジックが含まれるファイルです。
    • runtime·mallocgc関数内で、settype_bufsettype_bufsizeに関連するコードが削除されました。
    • MCacheListの代わりにMSpanを直接扱うように変更されました。
    • largealloc関数の戻り値がvoid*からMSpan*に変更されました。
    • settype関数の呼び出しが変更され、MSpanを直接渡すようになりました。
    • runtime·free関数内で、MCache_Freeの呼び出しロジックが変更されました。
  • src/pkg/runtime/malloc.h: メモリ管理関連の構造体と関数の宣言が含まれるヘッダファイルです。
    • MCache構造体の定義が大幅に変更され、MCacheList list[NumSizeClasses]MSpan* alloc[NumSizeClasses]MCacheList free[NumSizeClasses]に置き換えられました。
    • MSpan構造体にincacheフィールドとfreebufフィールドが追加されました。
    • MCache_RefillMCache_Freeの関数シグネチャが変更されました。
    • MCentral構造体のコメントが更新され、nonemptyemptyリストの役割が明確化されました。
    • runtime·markfreedのシグネチャからuintptr n引数が削除されました。
    • runtime·settype_flushruntime·settype_sysfreeの宣言が削除されました。
  • src/pkg/runtime/mcache.c: MCacheの実装ファイルです。
    • runtime·allocmcache関数が変更され、MCacheの初期化時にalloc配列がemptymspanで初期化されるようになりました。
    • runtime·MCache_Refill関数が大幅に書き換えられ、MCentralからMSpanを取得し、それをMCacheのalloc配列に設定するようになりました。
    • runtime·MCache_Free関数が変更され、オブジェクトをfreeリストに追加し、必要に応じてMCentralに返すようになりました。
    • runtime·MCache_ReleaseAll関数が変更され、alloc配列内のMSpanもMCentralに返すようになりました。
  • src/pkg/runtime/mcentral.c: MCentralの実装ファイルです。
    • runtime·MCentral_AllocListruntime·MCentral_CacheSpanに名称変更され、MCacheに割り当てるためのMSpanを返すように変更されました。
    • runtime·MCentral_UncacheSpan関数が追加され、MCacheから返されたMSpanを処理するようになりました。
    • MCentral_Free関数内で、s->incacheのチェックが追加され、MCacheが使用中のMSpanへの解放はfreebufに格納されるようになりました。
    • MCentral_FreeSpan関数内で、s->incacheのチェックが追加され、sweepgenの更新タイミングが変更されました。
    • MCentral_ReturnToHeapというヘルパー関数が追加され、MSpanをヒープに返すロジックがカプセル化されました。
  • src/pkg/runtime/mgc0.c: ガベージコレクション関連のコードが含まれるファイルです。
    • gc関数からruntime·settype_flush(mp)の呼び出しが削除されました。
    • runtime·markscanruntime·markfreed関数からCAS操作が削除され、直接的なビットマップの書き込みに置き換えられました。
    • runtime·MSpan_Sweep関数内で、sweepgenの更新ロジックが変更されました。
  • src/pkg/runtime/mheap.c: MHeapの実装ファイルです。
    • runtime·MSpan_Init関数内で、span->incache = false;span->freebuf = nil;の初期化が追加されました。
  • src/pkg/runtime/runtime.h: ランタイムのグローバルな定義が含まれるヘッダファイルです。
    • M構造体からsettype_bufsettype_bufsizeフィールドが削除されました。

コアとなるコードの解説

このコミットの主要な変更は、Goランタイムのメモリ割り当てとGCの連携方法を根本的に見直した点にあります。

1. settypeの変更 (malloc.goc, mgc0.c, runtime.h)

以前は、オブジェクトの型情報をM(OSスレッド)に紐付けられたsettype_bufに一時的にバッファリングし、バッファが満杯になったりGCがトリガーされたりした際に、グローバルロックsettype_lockを取得してsettype_flush関数でまとめて処理していました。このグローバルロックが並行処理のボトルネックでした。

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -175,21 +177,12 @@ runtime·mallocgc(uintptr size, uintptr typ, uint32 flag)
  if(DebugTypeAtBlockEnd)
  	*(uintptr*)((uintptr)v+size-sizeof(uintptr)) = typ;
 
+	m->mallocing = 0;
  // TODO: save type even if FlagNoScan?  Potentially expensive but might help
  // heap profiling/tracing.
-	if(UseSpanType && !(flag & FlagNoScan) && typ != 0) {
-		uintptr *buf, i;
-
-		buf = m->settype_buf;
-		i = m->settype_bufsize;
-		buf[i++] = (uintptr)v;
-		buf[i++] = typ;
-		m->settype_bufsize = i;
-	}
+	if(UseSpanType && !(flag & FlagNoScan) && typ != 0)
+		settype(s, v, typ);
 
-	m->mallocing = 0;
-	if(UseSpanType && !(flag & FlagNoScan) && typ != 0 && m->settype_bufsize == nelem(m->settype_buf))
-		runtime·settype_flush(m);
  if(raceenabled)
  	runtime·racemalloc(v, size);

この変更により、runtime·mallocgc内でsettype_bufへの書き込みとsettype_flushの呼び出しが削除され、代わりにsettype(s, v, typ)が直接呼び出されるようになりました。ここでsは割り当てられたオブジェクトvが属するMSpanです。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2302,9 +2310,6 @@ gc(struct gc_args *args)
  if(CollectStats)
  	runtime·memclr((byte*)&gcstats, sizeof(gcstats));
 
-	for(mp=runtime·allm; mp; mp=mp->alllink)
-		runtime·settype_flush(mp);
-
  m->locks++;	// disable gc during mallocs in parforalloc
  if(work.markfor == nil)
  	work.markfor = runtime·parforalloc(MaxGcproc);

GCの開始時に全てのMに対してsettype_flushを呼び出す処理も削除されました。これは、型情報が即座にMSpanに記録されるようになったため、GC開始時にバッファをフラッシュする必要がなくなったことを意味します。

2. MCacheMSpanの排他的所有権 (malloc.h, mcache.c, mcentral.c)

この変更の核心は、MCacheが特定のsizeclassに対してMSpanを排他的に所有するようになったことです。

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -301,7 +301,8 @@ struct MCache
  byte*	tiny;
  uintptr	tinysize;
  // The rest is not accessed on every malloc.
-	MCacheList list[NumSizeClasses];
+	MSpan*	alloc[NumSizeClasses];	// spans to allocate from
+	MCacheList free[NumSizeClasses];// lists of explicitly freed objects
  // Local allocator stats, flushed during GC.
  uintptr local_nlookup;		// number of pointer lookups
  uintptr local_largefree;	// bytes freed for large objects (>MaxSmallSize)
@@ -309,8 +310,9 @@ struct MCache
  uintptr local_nsmallfree[NumSizeClasses];	// number of frees for small objects (<=MaxSmallSize)
 };
 
-void	runtime·MCache_Refill(MCache *c, int32 sizeclass);
-void	runtime·MCache_Free(MCache *c, void *p, int32 sizeclass, uintptr size);
+MSpan*	runtime·MCache_Refill(MCache *c, int32 sizeclass);
+void	runtime·MCache_Free(MCache *c, MLink *p, int32 sizeclass, uintptr size);
 void	runtime·MCache_ReleaseAll(MCache *c);

MCache構造体は、list配列の代わりにallocfree配列を持つようになりました。alloc[sizeclass]は、そのMCacheが現在割り当てを行っているMSpanへのポインタを保持します。

--- a/src/pkg/runtime/mcache.c
+++ b/src/pkg/runtime/mcache.c
@@ -10,69 +10,119 @@
  #include "arch_GOARCH.h"
  #include "malloc.h"
  
+extern volatile intgo runtime·MemProfileRate;
+
+// dummy MSpan that contains no free objects.
+static MSpan emptymspan;
+
+MCache*
+runtime·allocmcache(void)
+{
+	intgo rate;
+	MCache *c;
+	int32 i;
+
+	runtime·lock(&runtime·mheap);
+	c = runtime·FixAlloc_Alloc(&runtime·mheap.cachealloc);
+	runtime·unlock(&runtime·mheap);
+	runtime·memclr((byte*)c, sizeof(*c));
+	for(i = 0; i < NumSizeClasses; i++)
+		c->alloc[i] = &emptymspan;
+
+	// Set first allocation sample size.
+	rate = runtime·MemProfileRate;
+	if(rate > 0x3fffffff)	// make 2*rate not overflow
+		rate = 0x3fffffff;
+	if(rate != 0)
+		c->next_sample = runtime·fastrand1() % (2*rate);
+
+	return c;
+}
+
 void
+runtime·freemcache(MCache *c)
+{
+	runtime·MCache_ReleaseAll(c);
+	runtime·lock(&runtime·mheap);
+	runtime·purgecachedstats(c);
+	runtime·FixAlloc_Free(&runtime·mheap.cachealloc, c);
+	runtime·unlock(&runtime·mheap);
+}
+
+// Gets a span that has a free object in it and assigns it
+// to be the cached span for the given sizeclass.  Returns this span.
+MSpan*
 runtime·MCache_Refill(MCache *c, int32 sizeclass)
  {
  	MCacheList *l;
+	MSpan *s;
  
-	// Replenish using central lists.
-	l = &c->list[sizeclass];
-	if(l->list)
-		runtime·throw("MCache_Refill: the list is not empty");
-	l->nlist = runtime·MCentral_AllocList(&runtime·mheap.central[sizeclass], &l->list);
-	if(l->list == nil)
-		runtime·throw("out of memory");
-}
+	m->locks++;
+	// Return the current cached span to the central lists.
+	s = c->alloc[sizeclass];
+	if(s->freelist != nil)
+		runtime·throw("refill on a nonempty span");
+	if(s != &emptymspan)
+		runtime·MCentral_UncacheSpan(&runtime·mheap.central[sizeclass], s);
  
-// Take n elements off l and return them to the central free list.
-static void
-ReleaseN(MCacheList *l, int32 n, int32 sizeclass)
-{
-	MLink *first, **lp;
-	int32 i;
+	// Push any explicitly freed objects to the central lists.
+	// Not required, but it seems like a good time to do it.
+	l = &c->free[sizeclass];
+	if(l->nlist > 0) {
+		runtime·MCentral_FreeList(&runtime·mheap.central[sizeclass], l->list);
+		l->list = nil;
+		l->nlist = 0;
+	}
  
-	// Cut off first n elements.
-	first = l->list;
-	lp = &l->list;
-	for(i=0; i<n; i++)
-		lp = &(*lp)->next;
-	l->list = *lp;
-	*lp = nil;
-	l->nlist -= n;
-
-	// Return them to central free list.
-	runtime·MCentral_FreeList(&runtime·mheap.central[sizeclass], first);
-}
+	// Get a new cached span from the central lists.
+	s = runtime·MCentral_CacheSpan(&runtime·mheap.central[sizeclass]);
+	if(s == nil)
+		runtime·throw("out of memory");
+	if(s->freelist == nil) {
+		runtime·printf("%d %d\\n", s->ref, (int32)((s->npages << PageShift) / s->elemsize));
+		runtime·throw("empty span");
+	}
+	c->alloc[sizeclass] = s;
+	m->locks--;
+	return s;
  }

runtime·MCache_Refillは、MCentralから新しいMSpanを取得し、それをc->alloc[sizeclass]に設定するようになりました。これにより、MCacheは特定のMSpanを排他的に所有し、そのMSpanからの割り当てはロックフリーで行えるようになります。

--- a/src/pkg/runtime/mcentral.c
+++ b/src/pkg/runtime/mcentral.c
@@ -30,12 +31,9 @@ runtime·MCentral_Init(MCentral *c, int32 sizeclass)
  	runtime·MSpanList_Init(&c->empty);
  }
  
-// Allocate a list of objects from the central free list.
-// Return the number of objects allocated.
-// The objects are linked together by their first words.
-// On return, *pfirst points at the first object.
-int32
-runtime·MCentral_AllocList(MCentral *c, MLink **pfirst)
+// Allocate a span to use in an MCache.
+MSpan*
+runtime·MCentral_CacheSpan(MCentral *c)
  {
  	MSpan *s;
  	int32 cap, n;
@@ -85,25 +83,63 @@ retry:
  	// Replenish central list if empty.
  	if(!MCentral_Grow(c)) {
  		runtime·unlock(c);
-		*pfirst = nil;
-		return 0;
+		return nil;
  	}
-	s = c->nonempty.next;
+	goto retry;
  
  havespan:
  	cap = (s->npages << PageShift) / s->elemsize;
  	n = cap - s->ref;
-	*pfirst = s->freelist;
-	s->freelist = nil;
-	s->ref += n;
+	if(n == 0)
+		runtime·throw("empty span");
+	if(s->freelist == nil)
+		runtime·throw("freelist empty");
  	c->nfree -= n;
  	runtime·MSpanList_Remove(s);
  	runtime·MSpanList_InsertBack(&c->empty, s);
+	s->incache = true;
+	runtime·unlock(c);
+	return s;
+}
+
+// Return span from an MCache.
+void
+runtime·MCentral_UncacheSpan(MCentral *c, MSpan *s)
+{
+	MLink *v;
+	int32 cap, n;
+
+	runtime·lock(c);
+
+	s->incache = false;
+
+	// Move any explicitly freed items from the freebuf to the freelist.
+	while((v = s->freebuf) != nil) {
+		s->freebuf = v->next;
+		runtime·markfreed(v);
+		v->next = s->freelist;
+		s->freelist = v;
+		s->ref--;
+	}
+
+	if(s->ref == 0) {
+		// Free back to heap.  Unlikely, but possible.
+		MCentral_ReturnToHeap(c, s); // unlocks c
+		return;
+	}
+	
+	cap = (s->npages << PageShift) / s->elemsize;
+	n = cap - s->ref;
+	if(n > 0) {
+		c->nfree += n;
+		runtime·MSpanList_Remove(s);
+		runtime·MSpanList_Insert(&c->nonempty, s);
+	}
  	runtime·unlock(c);
-	return n;
  }

runtime·MCentral_AllocListruntime·MCentral_CacheSpanに名称変更され、MSpan*を直接返すようになりました。また、runtime·MCentral_UncacheSpanが追加され、MCacheから返されたMSpanをMCentralに戻す処理を担います。この関数は、MCacheが所有していたMSpanのfreebuf(MCacheが所有中に明示的に解放されたオブジェクトのバッファ)を処理し、必要に応じてMSpanをヒープに返します。

3. ヒープビットマップ操作の簡素化 (mgc0.c)

markscanmarkfreed関数は、ヒープビットマップを更新する際にCAS操作を使用する必要がなくなりました。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2617,59 +2622,30 @@ runtime·marknogc(void *v)
 void
 runtime·markscan(void *v)
  {
-	uintptr *b, obits, bits, off, shift;
+	uintptr *b, off, shift;
  
  	off = (uintptr*)v - (uintptr*)runtime·mheap.arena_start;  // word offset
  	b = (uintptr*)runtime·mheap.arena_start - off/wordsPerBitmapWord - 1;
  	shift = off % wordsPerBitmapWord;
-
-	for(;;) {
-		obits = *b;
-		if((obits>>shift & bitMask) != bitAllocated)
-			runtime·throw("bad initial state for markscan");
-		bits = obits | bitScan<<shift;
-		if(runtime·gomaxprocs == 1) {
-			*b = bits;
-			break;
-		} else {
-			// more than one goroutine is potentially running: use atomic op
-			if(runtime·casp((void**)b, (void*)obits, (void*)bits))
-				break;
-		}
-	}
+	*b |= bitScan<<shift;
  }
  
-// mark the block at v of size n as freed.
+// mark the block at v as freed.
 void
-runtime·markfreed(void *v, uintptr n)
+runtime·markfreed(void *v)
  {
-	uintptr *b, obits, bits, off, shift;
+	uintptr *b, off, shift;
  
  	if(0)
-		runtime·printf("markfreed %p+%p\\n", v, n);
+		runtime·printf("markfreed %p\\n", v);
  
-	if((byte*)v+n > (byte*)runtime·mheap.arena_used || (byte*)v < runtime·mheap.arena_start)
+	if((byte*)v > (byte*)runtime·mheap.arena_used || (byte*)v < runtime·mheap.arena_start)
  		runtime·throw("markfreed: bad pointer");
  
  	off = (uintptr*)v - (uintptr*)runtime·mheap.arena_start;  // word offset
  	b = (uintptr*)runtime·mheap.arena_start - off/wordsPerBitmapWord - 1;
  	shift = off % wordsPerBitmapWord;
-
-	for(;;) {
-		obits = *b;
-		// This could be a free of a gc-eligible object (bitAllocated + others) or
-		// a FlagNoGC object (bitBlockBoundary set).  In either case, we revert to
-		// a simple no-scan allocated object because it is going on a free list.
-		bits = (obits & ~(bitMask<<shift)) | (bitAllocated<<shift);
-		if(runtime·gomaxprocs == 1) {
-			*b = bits;
-			break;
-		} else {
-			// more than one goroutine is potentially running: use atomic op
-			if(runtime·casp((void**)b, (void*)obits, (void*)bits))
-				break;
-		}
-	}
+	*b = (*b & ~(bitMask<<shift)) | (bitAllocated<<shift);
  }

markscanmarkfreedからCASループが削除され、直接的なビット操作に置き換えられました。これは、MCacheMSpanを排他的に所有することで、これらのビットマップ操作が競合しないことが保証されるためです。これにより、これらの操作のオーバーヘッドが削減されます。

これらの変更は、Goランタイムのメモリ割り当てとGCの並行性を大幅に向上させ、特にマルチコア環境でのアプリケーションのパフォーマンスを改善する上で重要な役割を果たしています。

関連リンク

  • Goのメモリ管理に関する公式ドキュメントやブログ記事 (当時のものがあれば)
  • Goのガベージコレクションに関する詳細な解説
  • Goのスケジューラに関する情報

参考にした情報源リンク

  • https://golang.org/cl/46810043 (このコミットのGo Code Reviewサイトのリンク)
  • Goのソースコード (特にsrc/runtimeディレクトリ)
  • Goのメモリ管理に関する一般的な知識と資料
  • Goのガベージコレクションに関する一般的な知識と資料
  • Goのスケジューラに関する一般的な知識と資料