[インデックス 15871] ファイルの概要
このコミットは、Goランタイムのガベージコレクタ(GC)における文字列のマーキング処理を最適化するものです。具体的には、文字列をマークする際に中間バッファを経由しないように変更することで、GCの効率を向上させています。
コミット
- Author: Jan Ziak 0xe2.0x9a.0x9b@gmail.com
- Date: Thu Mar 21 19:00:02 2013 +0100
- Commit Message:
runtime: mark strings without going through an intermediate buffer R=rsc CC=golang-dev https://golang.org/cl/7949043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ec8caf696ae44b9ea4624025a613dad4301398c4
元コミット内容
Goランタイムにおいて、ガベージコレクタが文字列をマークする際に、中間バッファを介さずに直接マークするように変更します。
変更の背景
Go言語のガベージコレクタは、プログラムが使用しなくなったメモリを自動的に解放する役割を担っています。このプロセスには、到達可能なオブジェクト(まだ使用されているオブジェクト)を「マーク」するフェーズが含まれます。このコミットの背景には、文字列オブジェクトのマーキング処理において、不要な中間バッファの使用がパフォーマンスのボトルネックとなっていた可能性があります。
中間バッファを介するということは、文字列オブジェクトのアドレスを一時的に別のメモリ領域にコピーし、そこから実際のマーキング処理を行うという手順を踏んでいたことを意味します。この余分なコピー操作は、特に大量の文字列が生成・破棄されるようなアプリケーションにおいて、GCのレイテンシやスループットに悪影響を与える可能性があります。
この変更は、文字列のマーキングパスを短縮し、より直接的にすることで、GCの効率を改善し、全体的なランタイムパフォーマンスの向上を目指しています。
前提知識の解説
Go言語のガベージコレクション (GC)
Go言語は、トレース型ガベージコレクタを採用しています。これは、プログラムが参照しているオブジェクトを追跡し、参照されていないオブジェクトを「ガベージ」として認識して解放する方式です。GoのGCは、主に以下のフェーズで動作します。
-
マークフェーズ (Mark Phase):
- GCのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトを特定し、それらを「マーク」します。マークされたオブジェクトは、まだ使用中であると判断され、解放の対象から外れます。
- GoのGCは、並行マーク(Concurrent Mark)を採用しており、アプリケーションの実行と並行してマーク処理を進めることで、ストップ・ザ・ワールド(STW: アプリケーションの実行を一時停止する時間)を最小限に抑えています。
- このコミットが関連するのは、このマークフェーズにおけるオブジェクトの走査とマーキングの具体的な実装です。
-
スウィープフェーズ (Sweep Phase):
- マークされなかったオブジェクト(ガベージ)をメモリから解放し、そのメモリを再利用可能な状態にします。
mgc0.c
と scanblock
関数
src/pkg/runtime/mgc0.c
は、Goランタイムのガベージコレクタのコア部分を実装しているC言語のファイルです。Goランタイムの多くの部分はGo言語で書かれていますが、GCのような低レベルでパフォーマンスが重要な部分はC言語(またはアセンブリ言語)で実装されています。
scanblock
関数は、GCのマークフェーズにおいて、特定のメモリブロック(オブジェクト)を走査し、そのブロック内のポインタが指す他のオブジェクトをマークするために使用されます。この関数は、オブジェクトの型情報(GCタイプ)に基づいて、どのようにポインタをたどるかを決定します。
GCタイプ (GC_STRING
, GC_DEFAULT_PTR
)
Goのオブジェクトは、そのメモリレイアウトと含まれるポインタの有無に基づいて、様々なGCタイプを持っています。
GC_STRING
: これは文字列オブジェクトのGCタイプを示します。Goの文字列は、内部的にはポインタと長さの構造体として表現されます。文字列自体は不変であり、その内容はヒープ上に確保されます。GCは、文字列のポインタが指す実際の文字列データが到達可能であることをマークする必要があります。GC_DEFAULT_PTR
: これは、一般的なポインタを含むオブジェクトのGCタイプを示します。構造体や配列など、複数のポインタを含む可能性があるオブジェクトを走査する際に使用されます。scanblock
関数は、このタイプのオブジェクトを走査する際に、含まれるすべてのポインタをたどってマークします。
中間バッファ
ガベージコレクションの文脈における「中間バッファ」とは、オブジェクトのアドレスやポインタを一時的に格納するために使用されるメモリ領域を指します。例えば、あるオブジェクトから別のオブジェクトへの参照をたどる際に、その参照先のアドレスを直接処理するのではなく、一度バッファに格納してから処理するといったケースが考えられます。このような中間バッファの使用は、コードの複雑性を増したり、余分なメモリコピーやキャッシュミスを引き起こし、パフォーマンスを低下させる可能性があります。
このコミットの目的は、文字列のマーキングにおいて、このような不要な中間バッファの使用を排除することです。
技術的詳細
このコミットは、src/pkg/runtime/mgc0.c
ファイル内の scanblock
関数に焦点を当てています。具体的には、GC_STRING
と GC_DEFAULT_PTR
の2つのケースにおけるポインタの処理方法が変更されています。
GC_STRING
ケースの変更
変更前は、GC_STRING
タイプの場合、文字列オブジェクトのアドレスを obj
に取得した後、markonly(obj)
を呼び出してマークし、その後 break
で switch
文を抜けていました。
変更後は、markonly(obj)
を呼び出した後、break
の代わりに continue
を使用しています。
- 変更前:
case GC_STRING: obj = *(void**)(stack_top.b + pc[1]); // markonly(obj); // この行は変更前にはなかったか、別の場所にあった可能性 pc += 2; break; // ここでswitch文を抜ける
- 変更後:
case GC_STRING: obj = *(void**)(stack_top.b + pc[1]); markonly(obj); // markonlyが追加または移動 pc += 2; continue; // ここで次のGCタイプ処理へ進む
この変更の意図は、GC_STRING
の処理が完了した後、scanblock
関数のループの次のイテレーションに直接進むようにすることです。これにより、GC_STRING
の処理がより効率的になり、不要なコードパスを回避できる可能性があります。元のコードでは、break
の後に続く共通の処理があった場合、それがスキップされることになります。continue
に変更することで、scanblock
のメインループの次の要素の処理に直接移ることができ、文字列のマーキングが完了した後に、その文字列オブジェクト自体がさらにポインタを保持しているかのように誤って処理されることを防ぎます。
GC_DEFAULT_PTR
ケースの変更
GC_DEFAULT_PTR
は、オブジェクト内に複数のポインタが含まれる可能性がある場合に、それらを順次走査してマークするためのケースです。
変更前は、while
ループ内で i = stack_top.b
と一時変数 i
に現在のポインタを格納し、その後 stack_top.b
をインクリメントしてから、obj = *(byte**)i
で i
からポインタを読み取っていました。
変更後は、一時変数 i
を使用せず、直接 obj = *(byte**)stack_top.b
で現在の stack_top.b
からポインタを読み取り、その後 stack_top.b
をインクリメントしています。
- 変更前:
case GC_DEFAULT_PTR: while((i = stack_top.b) <= end_b) { // iに一時的に格納 stack_top.b += PtrSize; obj = *(byte**)i; // iから読み取り if(obj >= arena_start && obj < arena_used) { *ptrbufpos++ = (PtrTarget){obj, 0}; if(ptrbufpos == ptrbuf_end) goto flushptrbuf; } } break;
- 変更後:
case GC_DEFAULT_PTR: while(stack_top.b <= end_b) { // iへの格納を削除 obj = *(byte**)stack_top.b; // 直接stack_top.bから読み取り stack_top.b += PtrSize; // その後インクリメント if(obj >= arena_start && obj < arena_used) { *ptrbufpos++ = (PtrTarget){obj, 0}; if(ptrbufpos == ptrbuf_end) goto flushptrbuf; } } break;
この変更は、まさに「中間バッファを経由しない」というコミットメッセージの意図を反映しています。i
という一時変数を介さずに、stack_top.b
が指すアドレスから直接ポインタを読み取ることで、余分なメモリ操作(i
への代入とそこからの読み取り)を排除し、コードパスを簡素化し、潜在的なパフォーマンス向上を図っています。これは、CPUのキャッシュ効率の改善にも寄与する可能性があります。
コアとなるコードの変更箇所
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -740,8 +740,9 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
case GC_STRING:
obj = *(void**)(stack_top.b + pc[1]);
+ markonly(obj);
pc += 2;
- break;
+ continue;
case GC_EFACE:
case GC_IFACE:
@@ -804,9 +805,9 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
break;
case GC_DEFAULT_PTR:
- while((i = stack_top.b) <= end_b) {
+ while(stack_top.b <= end_b) {
+ obj = *(byte**)stack_top.b;
stack_top.b += PtrSize;
- obj = *(byte**)i;
if(obj >= arena_start && obj < arena_used) {
*ptrbufpos++ = (PtrTarget){obj, 0};
if(ptrbufpos == ptrbuf_end)
コアとなるコードの解説
GC_STRING
の変更点
-
- break;
を+ continue;
に変更:- これは、
GC_STRING
の処理が完了した後に、scanblock
関数のメインループの次のイテレーションに直接進むことを意味します。 - 変更前は
break
でswitch
文を抜けていましたが、これによりscanblock
関数の残りの部分(switch
文の後に続く共通の処理など)が実行される可能性がありました。 continue
に変更することで、文字列のマーキング(markonly(obj)
)が完了したら、すぐに次のGCタイプまたは次のオブジェクトの走査に移るようになり、コードのフローがより直接的かつ効率的になります。これは、文字列オブジェクトがそれ自体でさらにポインタを保持しているかのように誤って処理されることを防ぐためにも重要です。
- これは、
-
+ markonly(obj);
の追加(または移動):- この行は、文字列オブジェクト
obj
を直接マークする関数呼び出しです。 - この変更により、文字列のポインタが指す実際のデータが、中間的なステップを介さずに直接GCによってマークされるようになります。これは、コミットメッセージにある「中間バッファを経由しない」という意図に合致します。
- この行は、文字列オブジェクト
GC_DEFAULT_PTR
の変更点
- while((i = stack_top.b) <= end_b) {
を+ while(stack_top.b <= end_b) {
に変更:- ループ条件から一時変数
i
への代入が削除されました。
- ループ条件から一時変数
- obj = *(byte**)i;
を+ obj = *(byte**)stack_top.b;
に変更:- ポインタの読み取り元が、一時変数
i
からstack_top.b
に直接変更されました。
- ポインタの読み取り元が、一時変数
stack_top.b += PtrSize;
の位置変更:- 変更前は
obj = *(byte**)i;
の前にstack_top.b
をインクリメントしていましたが、変更後はobj = *(byte**)stack_top.b;
の後にインクリメントしています。
- 変更前は
これらの変更は、GC_DEFAULT_PTR
のケースにおいて、ポインタを走査する際に一時変数 i
を使用するのをやめ、stack_top.b
から直接ポインタを読み取るようにすることで、中間的なメモリ操作を排除しています。これにより、コードがより簡潔になり、CPUのキャッシュ効率が向上し、全体的なパフォーマンスが改善される可能性があります。
まとめると、このコミットはGoランタイムのガベージコレクタにおいて、文字列および一般的なポインタのマーキング処理を最適化し、不要な中間バッファの使用を排除することで、GCの効率とランタイムパフォーマンスの向上を図っています。
関連リンク
- Go CL 7949043: https://golang.org/cl/7949043
参考にした情報源リンク
- Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (Web検索で得られた一般的な情報)
- Goランタイムのソースコード (
src/pkg/runtime/mgc0.c
の周辺コード) - ガベージコレクションの一般的な概念に関する情報