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

[インデックス 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は、主に以下のフェーズで動作します。

  1. マークフェーズ (Mark Phase):

    • GCのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトを特定し、それらを「マーク」します。マークされたオブジェクトは、まだ使用中であると判断され、解放の対象から外れます。
    • GoのGCは、並行マーク(Concurrent Mark)を採用しており、アプリケーションの実行と並行してマーク処理を進めることで、ストップ・ザ・ワールド(STW: アプリケーションの実行を一時停止する時間)を最小限に抑えています。
    • このコミットが関連するのは、このマークフェーズにおけるオブジェクトの走査とマーキングの具体的な実装です。
  2. スウィープフェーズ (Sweep Phase):

    • マークされなかったオブジェクト(ガベージ)をメモリから解放し、そのメモリを再利用可能な状態にします。

mgc0.cscanblock 関数

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_STRINGGC_DEFAULT_PTR の2つのケースにおけるポインタの処理方法が変更されています。

GC_STRING ケースの変更

変更前は、GC_STRING タイプの場合、文字列オブジェクトのアドレスを obj に取得した後、markonly(obj) を呼び出してマークし、その後 breakswitch 文を抜けていました。

変更後は、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**)ii からポインタを読み取っていました。

変更後は、一時変数 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 関数のメインループの次のイテレーションに直接進むことを意味します。
    • 変更前は breakswitch 文を抜けていましたが、これにより 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言語のガベージコレクションに関する公式ドキュメントやブログ記事 (Web検索で得られた一般的な情報)
  • Goランタイムのソースコード (src/pkg/runtime/mgc0.c の周辺コード)
  • ガベージコレクションの一般的な概念に関する情報