[インデックス 17430] ファイルの概要
このコミットは、Goランタイムのガベージコレクション(GC)におけるmarkonly
関数のパフォーマンス改善を目的としています。具体的には、オブジェクトの割り当てビットをチェックする際に、flushptrbuf
関数が既に行っていたビットマップワード内の後方検索をmarkonly
関数にも適用することで、不要なスパン検索を削減し、GCの効率を向上させています。また、この変更には、GCのルックアップに関する統計情報を収集する機能も追加されています。
コミット
commit c51152f438bf7b348de915b7b25fed343ea2a758
Author: Carl Shapiro <cshapiro@google.com>
Date: Thu Aug 29 13:52:38 2013 -0700
runtime: check bitmap word for allocated bit in markonly
When searching for an allocated bit, flushptrbuf would search
backward in the bitmap word containing the bit of pointer
being looked-up before searching the span. This extra check
was not replicated in markonly which, instead, after not
finding an allocated bit for a pointer would directly look in
the span.
Using statistics generated from godoc, before this change span
lookups were, on average, more common than word lookups. It
was common for markonly to consult spans for one third of its
pointer lookups. With this change in place, what were
previously span lookups are overwhelmingly become by the word
lookups making the total number of span lookups a relatively
small fraction of the whole.
This change also introduces some statistics gathering about
lookups guarded by the CollectStats enum.
R=golang-dev, khr
CC=golang-dev
https://golang.org/cl/13311043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c51152f438bf7b348de915b7b25fed343ea2a758
元コミット内容
このコミットは、Goランタイムのガベージコレクション(GC)におけるmarkonly
関数の動作を修正し、パフォーマンスを改善するものです。
既存のflushptrbuf
関数は、ポインタの割り当てビットを検索する際に、まずビットマップワード内で後方検索を行い、それでも見つからない場合にスパン(メモリ領域の管理単位)を検索していました。しかし、markonly
関数ではこのビットマップワード内の後方検索が行われておらず、割り当てビットが見つからない場合に直接スパンを検索していました。
この非対称な動作により、markonly
関数はポインタのルックアップの約3分の1でスパンを頻繁に参照していました。これは、スパンのルックアップがビットマップワード内のルックアップよりもコストが高い操作であるため、GCの効率を低下させていました。
このコミットは、markonly
関数にもビットマップワード内の後方検索ロジックを追加することで、この非効率性を解消します。これにより、以前はスパンルックアップが必要だったケースの大部分がビットマップワード内のルックアップで解決されるようになり、全体のスパンルックアップの回数が大幅に削減されます。
さらに、この変更には、CollectStats
という列挙型によって制御される、ルックアップに関する統計情報を収集する機能も導入されています。これにより、GCのパフォーマンス特性をより詳細に分析できるようになります。
変更の背景
Goのガベージコレクションは、プログラムの実行中に不要になったメモリを自動的に解放する重要な役割を担っています。GCの効率は、Goアプリケーション全体のパフォーマンスに直接影響します。
このコミットの背景には、GoランタイムのGCにおける特定の最適化の欠如がありました。GoのGCは、メモリを管理するために「スパン(MSpan)」と「ビットマップ」という2つの主要なデータ構造を使用します。
- スパン (MSpan): 連続したメモリページのブロックを表し、特定のサイズのオブジェクトを格納するために使用されます。各スパンは、そのスパン内のオブジェクトに関するメタデータ(例えば、どのオブジェクトが割り当てられているか、どのオブジェクトがマークされているかなど)を保持します。
- ビットマップ: メモリ内の各オブジェクトの割り当て状態やポインタの有無などを効率的に追跡するために使用されるデータ構造です。ビットマップは、メモリ上のオブジェクトに対応するビットの配列として表現され、各ビットは特定のオブジェクトの状態を示します。
GoのGCサイクル中、マーキングフェーズでは、到達可能なオブジェクトを特定し、マークする必要があります。このプロセスでは、ポインタが指すオブジェクトが実際に割り当てられているかどうかを効率的に判断する必要があります。
既存のflushptrbuf
関数は、ポインタが指すメモリ位置が割り当て済みかどうかを判断する際に、まずそのポインタが属するビットマップワード内で後方検索を行い、割り当てビット(bitAllocated
)やブロック境界ビット(bitBlockBoundary
)を探していました。これは、ポインタがオブジェクトの先頭を正確に指しているとは限らず、オブジェクトの内部を指している可能性があるためです。後方検索によって、ポインタが属するオブジェクトの先頭を見つけることができます。この検索で割り当てビットが見つからない場合にのみ、よりコストの高いスパンルックアップ(MHeap_LookupMaybe
)を実行していました。
しかし、markonly
関数では、このビットマップワード内の後方検索ロジックが欠けていました。そのため、markonly
は、ポインタがオブジェクトの先頭を指していない場合、すぐにスパンルックアップにフォールバックしていました。コミットメッセージによると、この非効率性により、markonly
のポインタルックアップの約3分の1がスパンルックアップになっていました。これは、GCのパフォーマンスボトルネックとなっていました。
このコミットは、markonly
関数にflushptrbuf
と同様のビットマップワード内の後方検索ロジックを導入することで、この非効率性を解消し、GCの全体的なパフォーマンスを向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、Goランタイムのメモリ管理とガベージコレクションに関するいくつかの基本的な概念を理解しておく必要があります。
Goランタイムとメモリ管理
Goプログラムは、Goランタイム上で実行されます。Goランタイムは、メモリの割り当て、スケジューリング、ガベージコレクションなど、プログラムの実行に必要な低レベルの操作を管理します。
Goのメモリ管理は、ヒープと呼ばれる領域で行われます。ヒープは、プログラムが動的にメモリを割り当てる場所です。Goランタイムは、このヒープを効率的に管理するために、いくつかのデータ構造とアルゴリズムを使用します。
ガベージコレクション (GC)
Goは、自動メモリ管理、すなわちガベージコレクションを採用しています。開発者が手動でメモリを解放する必要はありません。GoのGCは、主に「マーク&スイープ」アルゴリズムのバリアントを使用しています。
- マーキングフェーズ: プログラムが使用している(到達可能な)オブジェクトを特定し、マークします。
- スイープフェーズ: マークされていない(到達不可能な)オブジェクトを「ガベージ」とみなし、それらが占めていたメモリを解放し、再利用可能にします。
このコミットは、マーキングフェーズにおけるオブジェクトのルックアップ(検索)の効率に関わるものです。
MSpan (Memory Span)
Goのメモリヒープは、MSpan
と呼ばれる連続したメモリページのブロックに分割されています。各MSpan
は、特定のサイズのオブジェクトを格納するために使用されます。例えば、小さなオブジェクト用のスパン、大きなオブジェクト用のスパンなどがあります。MSpan
構造体は、そのスパン内のメモリブロックに関するメタデータ(例えば、どのブロックが空いているか、どのブロックが割り当てられているかなど)を保持します。
ビットマップ
GoのGCは、メモリ内のオブジェクトの状態を効率的に追跡するためにビットマップを使用します。ビットマップは、メモリ上の各オブジェクトに対応するビットの配列です。各ビットは、そのオブジェクトが割り当てられているか、ポインタを含んでいるか、ブロックの境界であるかなどの情報を示します。
bitAllocated
: そのメモリ位置にオブジェクトが割り当てられていることを示すビット。bitBlockBoundary
: そのメモリ位置がオブジェクトのブロックの開始点であることを示すビット。
これらのビットは、GCがメモリをスキャンし、オブジェクトの境界を特定し、ポインタを追跡するために不可欠です。
markonly
関数
markonly
関数は、GoのGCのマーキングフェーズにおいて重要な役割を果たします。この関数は、特定のメモリアドレス(ポインタ)が指すオブジェクトをマークするために呼び出されます。markonly
の主なタスクは、与えられたポインタが有効なGoオブジェクトの内部を指しているかどうかを判断し、もしそうであればそのオブジェクトを「到達可能」としてマークすることです。
flushptrbuf
関数
flushptrbuf
関数は、GCのマーキングフェーズ中に、ポインタバッファに蓄積されたポインタを処理するために使用されます。GCは、プログラムの実行中に新しいポインタが作成されたり、既存のポインタが変更されたりするのを追跡します。これらのポインタは一時的にバッファに格納され、その後flushptrbuf
によって処理され、参照先のオブジェクトがマークされます。
CollectStats
CollectStats
は、Goランタイムのコンパイル時に設定できるフラグ(または列挙型)です。これが有効になっている場合、ランタイムは特定の内部統計情報(例えば、GCのルックアップ回数など)を収集します。これらの統計情報は、ランタイムのパフォーマンス特性を分析し、最適化の機会を特定するために役立ちます。
技術的詳細
このコミットの技術的詳細は、Goランタイムのメモリ管理におけるビットマップの利用と、オブジェクトの割り当て状態を効率的に判断するためのルックアップ戦略に焦点を当てています。
GoのGCは、ポインタが指すメモリ位置が実際に割り当てられたオブジェクトの一部であるかどうかを判断する必要があります。これは、ポインタがオブジェクトの先頭を正確に指しているとは限らず、オブジェクトの内部の任意の位置を指している可能性があるため、複雑になります。
ビットマップワード内の後方検索の重要性
Goのメモリヒープは、MSpan
と呼ばれるチャンクに分割され、各MSpan
はビットマップによって管理されます。ビットマップは、uintptr
のワード(通常は64ビット)の配列として格納され、各ワードはメモリの特定のブロックに対応します。各ビットは、そのメモリブロック内の特定のバイトオフセットに対応し、そのバイトがオブジェクトの先頭であるか、割り当てられているかなどの情報を示します。
ポインタobj
が与えられたとき、GCはまずそのポインタが属するビットマップワードを特定します。そして、そのワード内の対応するビットを調べます。しかし、ポインタがオブジェクトの先頭を指していない場合、そのビットだけではオブジェクトの境界を特定できません。
ここで、「ビットマップワード内の後方検索」が重要になります。これは、ポインタが指すビットから、そのビットマップワードの先頭に向かって逆方向にスキャンし、bitAllocated
またはbitBlockBoundary
ビットが設定されている最初の位置を見つけるプロセスです。この位置が、ポインタが指すオブジェクトの実際の先頭である可能性が高いです。
この後方検索は、スパン全体を検索するよりもはるかに高速です。スパン検索(MHeap_LookupMaybe
)は、より広範なメモリ領域をカバーしますが、その分オーバーヘッドが大きくなります。したがって、ビットマップワード内の後方検索でオブジェクトの先頭を特定できれば、GCのパフォーマンスが大幅に向上します。
markonly
とflushptrbuf
の非対称性
コミットメッセージが指摘するように、以前はflushptrbuf
関数はこのビットマップワード内の後方検索を行っていましたが、markonly
関数は行っていませんでした。
flushptrbuf
の動作: ポインタが指すビットマップワード内で後方検索を行い、割り当てビットまたはブロック境界ビットを見つけようとします。見つからない場合にのみ、MHeap_LookupMaybe
を呼び出してスパン全体を検索します。markonly
の以前の動作: ポインタが指すビットがbitAllocated
またはbitBlockBoundary
でない場合、すぐにMHeap_LookupMaybe
を呼び出してスパン全体を検索していました。ビットマップワード内の後方検索をスキップしていました。
この非対称性により、markonly
は不必要に高コストなスパンルックアップを頻繁に実行していました。
統計情報の追加
このコミットでは、GCのルックアップに関する統計情報を収集するための新しいフィールドがgcstats
構造体に追加されています。
typedef struct {
// ... 既存のフィールド ...
struct {
uint64 foundbit;
uint64 foundword;
uint64 foundspan;
} flushptrbuf;
struct {
uint64 foundbit;
uint64 foundword;
uint64 foundspan;
} markonly;
} gcstats;
foundbit
: ポインタが指すビットが直接bitAllocated
またはbitBlockBoundary
であった場合にカウントされます。foundword
: ビットマップワード内の後方検索によってオブジェクトの先頭が見つかった場合にカウントされます。foundspan
: スパンルックアップ(MHeap_LookupMaybe
)によってオブジェクトの先頭が見つかった場合にカウントされます。
これらの統計情報は、CollectStats
が有効な場合にのみ収集され、GCの実行後にruntime·printf
によって出力されます。これにより、GCのルックアップ戦略の有効性を定量的に評価し、さらなる最適化の機会を特定できるようになります。
この変更は、GoのGCがより効率的にメモリをスキャンし、オブジェクトをマークできるようにすることで、全体的なアプリケーションのパフォーマンスを向上させることに貢献します。
コアとなるコードの変更箇所
変更は src/pkg/runtime/mgc0.c
ファイルに集中しています。
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -199,6 +199,16 @@ static struct {
uint64 instr[GC_NUM_INSTR2];
uint64 putempty;
uint64 getfull;
+ struct {
+ uint64 foundbit;
+ uint64 foundword;
+ uint64 foundspan;
+ } flushptrbuf;
+ struct {
+ uint64 foundbit;
+ uint64 foundword;
+ uint64 foundspan;
+ } markonly;
} gcstats;
// markonly marks an object. It returns true if the object
@@ -208,7 +218,7 @@ static bool
markonly(void *obj)
{
byte *p;
- uintptr *bitp, bits, shift, x, xbits, off;
+ uintptr *bitp, bits, shift, x, xbits, off, j;
MSpan *s;
PageID k;
@@ -230,8 +240,23 @@ markonly(void *obj)
bits = xbits >> shift;\n
// Pointing at the beginning of a block?
-\tif((bits & (bitAllocated|bitBlockBoundary)) != 0)\n
+\tif((bits & (bitAllocated|bitBlockBoundary)) != 0) {\n
+\t\tif(CollectStats)\n
+\t\t\truntime·xadd64(&gcstats.markonly.foundbit, 1);\n
\tgoto found;\n
+\t}\n
+\n+\t// Pointing just past the beginning?\n+\t// Scan backward a little to find a block boundary.\n+\tfor(j=shift; j-->0; ) {\n+\t\tif(((xbits>>j) & (bitAllocated|bitBlockBoundary)) != 0) {\n+\t\t\tshift = j;\n+\t\t\tbits = xbits>>shift;\n+\t\t\tif(CollectStats)\n+\t\t\t\truntime·xadd64(&gcstats.markonly.foundword, 1);\n+\t\t\tgoto found;\n+\t\t}\n+\t}\n
// Otherwise consult span table to find beginning.
// (Manually inlined copy of MHeap_LookupMaybe.)
@@ -257,6 +282,8 @@ markonly(void *obj)
shift = off % wordsPerBitmapWord;\n
xbits = *bitp;\n
bits = xbits >> shift;\n
+\tif(CollectStats)\n
+\t\truntime·xadd64(&gcstats.markonly.foundspan, 1);\n
found:\n
// Now we have bits, bitp, and shift correct for
@@ -395,8 +422,11 @@ flushptrbuf(PtrTarget *ptrbuf, PtrTarget **ptrbufpos, Obj **_wp, Workbuf **_wbuf
bits = xbits >> shift;\n
// Pointing at the beginning of a block?
-\t\t\tif((bits & (bitAllocated|bitBlockBoundary)) != 0)\n
+\t\t\tif((bits & (bitAllocated|bitBlockBoundary)) != 0) {\n
+\t\t\t\tif(CollectStats)\n
+\t\t\t\t\truntime·xadd64(&gcstats.flushptrbuf.foundbit, 1);\n
\tgoto found;\n
+\t\t\t}\n
tti = 0;\n
@@ -407,6 +437,8 @@ flushptrbuf(PtrTarget *ptrbuf, PtrTarget **ptrbufpos, Obj **_wp, Workbuf **_wbuf
obj = (byte*)obj - (shift-j)*PtrSize;\n
shift = j;\n
bits = xbits>>shift;\n
+\t\t\t\t\tif(CollectStats)\n
+\t\t\t\t\t\truntime·xadd64(&gcstats.flushptrbuf.foundword, 1);\n
\tgoto found;\n
}\n
}\n
@@ -435,6 +467,8 @@ flushptrbuf(PtrTarget *ptrbuf, PtrTarget **ptrbufpos, Obj **_wp, Workbuf **_wbuf
shift = off % wordsPerBitmapWord;\n
xbits = *bitp;\n
bits = xbits >> shift;\n
+\t\t\tif(CollectStats)\n
+\t\t\t\truntime·xadd64(&gcstats.flushptrbuf.foundspan, 1);\n
found:\n
// Now we have bits, bitp, and shift correct for
@@ -2233,6 +2267,9 @@ gc(struct gc_args *args)\n
\truntime·printf(\"\\ttotal:\\t%D\\n\", ninstr);\n
\truntime·printf(\"putempty: %D, getfull: %D\\n\", gcstats.putempty, gcstats.getfull);\n
+\n+\t\t\truntime·printf(\"markonly base lookup: bit %D word %D span %D\\n\", gcstats.markonly.foundbit, gcstats.markonly.foundword, gcstats.markonly.foundspan);\n+\t\t\truntime·printf(\"flushptrbuf base lookup: bit %D word %D span %D\\n\", gcstats.flushptrbuf.foundbit, gcstats.flushptrbuf.foundword, gcstats.flushptrbuf.foundspan);\n
}\n
}\n
コアとなるコードの解説
1. gcstats
構造体の拡張
gcstats
構造体に、flushptrbuf
とmarkonly
それぞれのルックアップ統計を格納するための新しいフィールドが追加されました。
struct {
uint64 foundbit;
uint64 foundword;
uint64 foundspan;
} flushptrbuf;
struct {
uint64 foundbit;
uint64 foundword;
uint64 foundspan;
} markonly;
foundbit
: ポインタが指す位置が直接割り当て済みまたはブロック境界であった回数。foundword
: ビットマップワード内の後方検索によってオブジェクトの先頭が見つかった回数。foundspan
: スパンルックアップ(MHeap_LookupMaybe
)によってオブジェクトの先頭が見つかった回数。
これらのカウンタは、CollectStats
が有効な場合にのみインクリメントされます。
2. markonly
関数の変更
markonly
関数に、ビットマップワード内の後方検索ロジックが追加されました。
// Pointing at the beginning of a block?
if((bits & (bitAllocated|bitBlockBoundary)) != 0) {
if(CollectStats)
runtime·xadd64(&gcstats.markonly.foundbit, 1);
goto found;
}
// Pointing just past the beginning?
// Scan backward a little to find a block boundary.
for(j=shift; j-->0; ) {
if(((xbits>>j) & (bitAllocated|bitBlockBoundary)) != 0) {
shift = j;
bits = xbits>>shift;
if(CollectStats)
runtime·xadd64(&gcstats.markonly.foundword, 1);
goto found;
}
}
- 最初の
if
ブロック: ポインタが指すビットが既にbitAllocated
またはbitBlockBoundary
である場合、つまりオブジェクトの先頭を直接指している場合は、foundbit
カウンタをインクリメントし、found
ラベルにジャンプします。これは既存のロジックです。 for
ループ: ここが新しい追加部分です。ポインタがオブジェクトの先頭を直接指していない場合、このループが実行されます。for(j=shift; j-->0; )
:shift
は、obj
がビットマップワードの先頭からどれだけオフセットしているかを示します。このループは、現在のshift
から0まで(つまり、ビットマップワードの先頭に向かって)逆方向にイテレートします。if(((xbits>>j) & (bitAllocated|bitBlockBoundary)) != 0)
: 各ビット位置j
で、そのビットがbitAllocated
またはbitBlockBoundary
であるかをチェックします。shift = j; bits = xbits>>shift;
: もし割り当てビットまたはブロック境界ビットが見つかった場合、新しいshift
とbits
を更新します。これにより、obj
が指すオブジェクトの実際の先頭が特定されます。runtime·xadd64(&gcstats.markonly.foundword, 1);
:foundword
カウンタをインクリメントします。goto found;
: オブジェクトの先頭が見つかったので、found
ラベルにジャンプして後続の処理に進みます。
この変更により、markonly
はflushptrbuf
と同様に、まずビットマップワード内の後方検索を試み、それでも見つからない場合にのみスパンルックアップにフォールバックするようになりました。
3. markonly
とflushptrbuf
におけるfoundspan
のインクリメント
markonly
とflushptrbuf
の両方で、スパンルックアップ(MHeap_LookupMaybe
)が実行された直後にfoundspan
カウンタがインクリメントされるようになりました。
// markonly
// Otherwise consult span table to find beginning.
// (Manually inlined copy of MHeap_LookupMaybe.)
// ...
if(CollectStats)
runtime·xadd64(&gcstats.markonly.foundspan, 1);
// flushptrbuf
// Otherwise consult span table to find beginning.
// (Manually inlined copy of MHeap_LookupMaybe.)
// ...
if(CollectStats)
runtime·xadd64(&gcstats.flushptrbuf.foundspan, 1);
これにより、スパンルックアップが実際に何回発生したかを正確に追跡できるようになります。
4. GC統計の出力
gc
関数(ガベージコレクションのメインエントリポイント)の最後に、新しいルックアップ統計が出力されるようになりました。
if(CollectStats) {
// ... 既存の統計出力 ...
runtime·printf("markonly base lookup: bit %D word %D span %D\n", gcstats.markonly.foundbit, gcstats.markonly.foundword, gcstats.markonly.foundspan);
runtime·printf("flushptrbuf base lookup: bit %D word %D span %D\n", gcstats.flushptrbuf.foundbit, gcstats.flushptrbuf.foundword, gcstats.flushptrbuf.foundspan);
}
この出力により、開発者はGCのルックアップ戦略がどれだけ効率的に機能しているかを把握し、さらなる最適化のヒントを得ることができます。コミットメッセージにある「godocから生成された統計」は、このような統計収集機能が既に存在し、今回の変更でさらに詳細な情報が追加されたことを示唆しています。
関連リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事:
- Go's Garbage Collector: A Brief History - The Go Programming Language (Go 1.5 GCに関する記事ですが、基本的な概念の理解に役立ちます)
- Go runtime source code (Goランタイムのソースコード)
- Goのメモリ管理に関する詳細な解説記事(非公式):
- "Go's Memory Allocator" や "Go's Garbage Collector" といったキーワードで検索すると、多くの技術ブログや論文が見つかります。
参考にした情報源リンク
- コミットメッセージ自体:
https://github.com/golang/go/commit/c51152f438bf7b348de915b7b25fed343ea2a758
- Goのソースコード:
src/pkg/runtime/mgc0.c
- Goのガベージコレクションとメモリ管理に関する一般的な知識 (Go公式ドキュメント、技術ブログ、論文など)
- Goの
runtime
パッケージのドキュメント (GoDoc) runtime·xadd64
のようなGoランタイム内部関数に関する情報 (Goのソースコードや関連する技術記事)bitAllocated
,bitBlockBoundary
などのビットマップフラグに関する情報 (Goのソースコード)I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The output is provided to standard output only, as requested.