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

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

このコミットは、Goランタイムにおいて、一部の静的変数をガベージコレクタ(GC)がポインタを含まないと認識するセクション(.noptrセクションに相当)に移動させることで、GCのパフォーマンスを向上させる変更です。特に32ビット実行ファイルにおけるGCの効率化と、symtab.c内のfiles変数の動的割り当てへの変更が主な内容です。

コミット

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

https://github.com/golang/go/commit/e017e0cb24f9c5ea4c0e2b7479e4b411e4882dcf

元コミット内容

runtime: flag static variables as no-pointers

Variables in data sections of 32-bit executables interfere with
garbage collector's ability to free objects and/or unnecessarily
slow down the garbage collector.

This changeset moves some static variables to .noptr sections.
'files' in symtab.c is now allocated dynamically.

R=golang-dev, dvyukov, minux.ma
CC=golang-dev
https://golang.org/cl/9786044

変更の背景

この変更の主な背景は、Goランタイムのガベージコレクタ(GC)のパフォーマンス最適化、特に32ビット実行環境における問題の解消です。

GoのGCは、メモリ上のオブジェクトがまだ使用されているかどうかを判断するために、ポインタを追跡します。データセクションに存在する静的変数の中にポインタが含まれていると、GCはそれらのポインタをスキャンして到達可能なオブジェクトを特定する必要があります。しかし、実際にはポインタを含まない静的変数であっても、GCはそれらをスキャンしようとするため、GCの処理が不必要に遅くなったり、場合によってはオブジェクトの解放を妨げたりする可能性がありました。

特に32ビットシステムでは、メモリ空間が限られているため、GCの効率はより重要になります。このコミットは、ポインタを含まないことが保証されている静的変数を、GCがスキャンする必要のない特別なメモリセクション(概念的には.noptrセクション)に配置することで、この問題を解決しようとしています。これにより、GCはこれらの変数を無視できるため、スキャン時間が短縮され、全体的なGCのパフォーマンスが向上します。

また、symtab.c内のfiles変数が静的に確保されていたため、そのサイズが固定されており、シンボルテーブルの処理において柔軟性に欠ける可能性がありました。これを動的に割り当てることで、より効率的なメモリ管理とスケーラビリティが実現されます。

前提知識の解説

ガベージコレクション (GC) とGoのGC

ガベージコレクション(GC)は、プログラムが動的に割り当てたメモリのうち、もはや使用されていない(到達不可能になった)ものを自動的に解放する仕組みです。これにより、プログラマは手動でのメモリ管理の複雑さから解放され、メモリリークのリスクを低減できます。

Go言語のGCは、**並行(concurrent)かつ三色マーク&スイープ(tri-color mark-and-sweep)**アルゴリズムをベースにしています。

  1. マークフェーズ: GCは、プログラムの実行と並行して、ルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトを「マーク」します。GoのGCは、ポインタを追跡することで到達可能性を判断します。
  2. スイープフェーズ: マークされなかった(到達不可能な)オブジェクトのメモリを解放します。

GCの効率は、マークフェーズでスキャンする必要があるメモリ領域の量に大きく依存します。ポインタを含まないことが分かっているメモリ領域は、GCがスキャンする必要がないため、GCのオーバーヘッドを大幅に削減できます。

静的変数とデータセクション

静的変数は、プログラムの実行開始時にメモリに割り当てられ、プログラムの終了時までその寿命が続く変数です。これらは通常、プログラムのバイナリイメージの一部として、データセクション.data.rodataなど)に格納されます。

  • .dataセクション: 初期化された読み書き可能なデータ(グローバル変数、静的変数など)。
  • .rodataセクション: 読み取り専用のデータ(文字列リテラル、定数など)。

GCは、これらのデータセクションもスキャンしてポインタを探す必要があります。

.noptrセクション(概念)

Goランタイムの文脈における.noptrセクションは、GCがスキャンする必要のない、ポインタを含まないデータが格納されるメモリ領域を指す概念です。これは、実際のELF/Mach-O/PEバイナリのセクション名として直接存在するわけではなく、Goランタイムが内部的に管理するメモリ領域の特性を示します。

GCは、この.noptrセクションに配置されたデータについては、ポインタスキャンをスキップできます。これにより、GCの作業量が減り、パフォーマンスが向上します。Goのコンパイラは、エスケープ解析などを用いて、変数がポインタを含まないことを判断し、適切なメモリ領域に配置する最適化を行います。

#pragma dataflag 16

#pragmaディレクティブは、コンパイラに対する特別な指示です。Goの初期のランタイムコード(特にC言語で書かれていた部分)では、C言語の#pragmaが使用されていました。

#pragma dataflag 16は、Goの内部的なコンパイラ/リンカフラグであり、特定のデータがポインタを含まないことを示すために使用されました。16という値は、内部的なビットマスクの特定のフラグに対応しており、このフラグが設定されたデータはGCによってポインタスキャンがスキップされることを意味します。

現代のGoでは、このようなCスタイルの#pragmaはほとんど使用されず、代わりに//go:directive形式の特殊なコメント(例: //go:nosplit, //go:noescape)がコンパイラディレクティブとして使われています。しかし、このコミットが作成された2013年当時は、このような#pragmaがGoランタイムの低レベルな最適化に利用されていました。

技術的詳細

このコミットは、Goランタイムのガベージコレクタ(GC)の効率を向上させるために、静的変数のメモリ配置を最適化しています。

  1. ポインタフリーデータの識別と配置: GoのGCは、メモリ上のポインタを追跡して到達可能なオブジェクトを特定します。もし、あるメモリ領域がポインタを一切含まないことが分かっていれば、GCはその領域をスキャンする必要がありません。これにより、GCのマークフェーズの時間が短縮され、全体的なGCのレイテンシとスループットが改善されます。 このコミットでは、src/pkg/runtime/hashmap.csrc/pkg/runtime/os_darwin.csrc/pkg/runtime/os_freebsd.csrc/pkg/runtime/os_linux.csrc/pkg/runtime/os_netbsd.csrc/pkg/runtime/os_openbsd.csrc/pkg/runtime/os_plan9.csrc/pkg/runtime/os_windows.cといったOS固有のランタイムファイルやハッシュマップの実装において、一部の静的変数(例: empty_value, badcallback, badsignal, urandom_dataなど)に対して#pragma dataflag 16 // no pointersというディレクティブを追加しています。 このディレクティブは、コンパイラに対して、これらの変数がポインタを含まないことを明示的に伝え、結果としてこれらの変数がGCのポインタスキャン対象から除外されるように、特別なメモリセクション(概念的な.noptrセクション)に配置されることを指示します。

  2. symtab.cにおけるfiles変数の動的割り当て: src/pkg/runtime/symtab.cは、Goのシンボルテーブル(プログラムカウンタとファイル/行番号のマッピングなど)を管理する重要なファイルです。以前のバージョンでは、filesという構造体の配列が静的にfiles[200]として宣言されていました。これは、最大200個のファイル情報しか扱えないという固定的な制限を意味し、また、この静的配列がデータセクションに配置されるため、GCの対象となり得ました。 このコミットでは、files変数を静的配列からポインタ*filesに変更し、runtime·malloc関数を使用して動的にメモリを割り当てるように変更しています。

    // 変更前: 静的配列
    // static struct { ... } files[200];
    
    // 変更後: ポインタと動的割り当て
    static struct {
        String srcstring;
        int32 aline;
        int32 delta;
    } *files;
    // ...
    // files = runtime·malloc(maxfiles * sizeof(files[0]));
    

    この変更により、filesのメモリはヒープに割り当てられ、GCによって適切に管理されるようになります。また、maxfilesという定数(200)は引き続き使用されていますが、動的割り当てにすることで、将来的にこの制限を柔軟に変更できるようになります。 さらに、srcbufという一時的なバッファも#pragma dataflag 16でポインタフリーとしてマークされています。

これらの変更は、Goランタイムの低レベルな最適化であり、GCの効率を直接的に改善し、特にメモリが制約される32ビット環境でのパフォーマンス向上に寄与します。

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

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

  • src/pkg/runtime/hashmap.c
  • src/pkg/runtime/os_darwin.c
  • src/pkg/runtime/os_freebsd.c
  • src/pkg/runtime/os_linux.c
  • src/pkg/runtime/os_netbsd.c
  • src/pkg/runtime/os_openbsd.c
  • src/pkg/runtime/os_plan9.c
  • src/pkg/runtime/os_windows.c
  • src/pkg/runtime/symtab.c

主な変更点:

  1. #pragma dataflag 16 // no pointers の追加: 上記のhashmap.cおよびos_*.cファイルにおいて、empty_value, badcallback, badsignal, urandom_dataなどの静的変数宣言の直前に#pragma dataflag 16 // no pointersが追加されています。これにより、これらの変数がポインタを含まないデータとしてGCに認識され、GCスキャンから除外されます。

  2. src/pkg/runtime/symtab.c の変更:

    • files変数の宣言が静的配列からポインタに変更されました。
      --- a/src/pkg/runtime/symtab.c
      +++ b/src/pkg/runtime/symtab.c
      @@ -307,6 +307,15 @@ gostringn(byte *p, int32 l)
       	return s;
       }
      
      +static struct
      +{
      +	String srcstring;
      +	int32 aline;
      +	int32 delta;
      +} *files;
      +
      +enum { maxfiles = 200 };
      +
       // walk symtab accumulating path names for use by pc/ln table.
       // don't need the full generality of the z entry history stack because
       // there are no includes in go (and only sensible includes in our c);
      @@ -314,12 +323,8 @@ gostringn(byte *p, int32 l)
       static void
       dosrcline(Sym *sym)
       {
      +	#pragma dataflag 16 // no pointers
       	static byte srcbuf[1000];
      -	static struct {
      -		String srcstring;
      -		int32 aline;
      -		int32 delta;
      -	} files[200];
        	static int32 incstart;
        	static int32 nfunc, nfile, nhist;
        	Func *f;
      @@ -347,7 +352,7 @@ dosrcline(Sym *sym)
       		\tl = makepath(srcbuf, sizeof srcbuf, sym->name+1);\
       		\tnhist = 0;\
       		\tnfile = 0;\
      -\t\t\tif(nfile == nelem(files))\
      +\t\t\tif(nfile == maxfiles)\
       		\t\treturn;\
       		\tfiles[nfile].srcstring = gostringn(srcbuf, l);\
       		\tfiles[nfile].aline = 0;\
      @@ -358,7 +363,7 @@ dosrcline(Sym *sym)
       		\tif(srcbuf[0] != '\0') {\
       		\t\tif(nhist++ == 0)\
       		\t\t\tincstart = sym->value;\
      -\t\t\t\tif(nhist == 0 && nfile < nelem(files)) {\
      +\t\t\t\tif(nhist == 0 && nfile < maxfiles) {\
       		\t\t\t// new top-level file\
       		\t\t\tfiles[nfile].srcstring = gostringn(srcbuf, l);\
       		\t\t\tfiles[nfile].aline = sym->value;\
      @@ -567,10 +572,12 @@ buildfuncs(void)
        	splitpcln();
      
        	// record src file and line info for each func
      +	files = runtime·malloc(maxfiles * sizeof(files[0]));
        	walksymtab(dosrcline);  // pass 1: determine hugestring_len
        	hugestring.str = runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0);\
        	hugestring.len = 0;\
        	walksymtab(dosrcline);  // pass 2: fill and use hugestring
      +	files = nil;
      
        	if(hugestring.len != hugestring_len)\
        		runtime·throw("buildfunc: problem in initialization procedure");
      
    • dosrcline関数内のsrcbuf#pragma dataflag 16でマークされました。
    • buildfuncs関数内で、filesポインタにruntime·mallocを使ってメモリが動的に割り当てられ、処理の最後にfiles = nil;で解放されています。また、nelem(files)の代わりにmaxfilesが使用されるようになりました。

コアとなるコードの解説

pragma dataflag 16 // no pointers の適用

Goランタイムの様々なCソースファイルにおいて、静的に宣言されていた文字列やバッファに対して#pragma dataflag 16 // no pointersが追加されています。

  • src/pkg/runtime/hashmap.c: static uint8 empty_value[MAXVALUESIZE]; ハッシュマップで項目が見つからなかった場合に返されるゼロ値のメモリ領域。この領域はポインタを含まないため、GCスキャンから除外されます。

  • src/pkg/runtime/os_darwin.c, os_freebsd.c, os_linux.c, os_netbsd.c, os_openbsd.c, os_plan9.c, os_windows.c: static int8 badcallback[] = "runtime: cgo callback on thread not created by Go.\\n"; static int8 badsignal[] = "runtime: signal received on thread not created by Go: "; CGOコールバックやシグナル処理に関連するエラーメッセージ文字列。これらは単なる文字列リテラルであり、ポインタを含まないため、GCスキャンから除外されます。 os_linux.cでは、static byte urandom_data[HashRandomBytes];も同様にマークされています。

これらの変更は、GCがこれらの静的変数をスキャンする手間を省き、GCのオーバーヘッドを削減することを目的としています。

src/pkg/runtime/symtab.c における files 変数の変更

symtab.cは、Goの実行バイナリ内のシンボル情報(関数名、ファイル名、行番号など)を処理するGoランタイムの重要な部分です。

  • 静的配列から動的割り当てへの変更: 変更前は、filesというシンボルテーブルのエントリを保持する構造体の配列が、static struct { ... } files[200];として静的に宣言されていました。これは、最大200個のファイル情報しか処理できないという固定的な制限があり、また、この静的配列全体がGCの対象となり得るデータセクションに配置されていました。 変更後は、static struct { ... } *files;とポインタとして宣言され、buildfuncs関数内でfiles = runtime·malloc(maxfiles * sizeof(files[0]));によって動的にメモリが割り当てられるようになりました。 これにより、filesがヒープに割り当てられ、GCによって適切に管理されるようになります。また、maxfilesという定数(200)は引き続き使用されていますが、動的割り当てにすることで、将来的にこの制限を柔軟に変更することが可能になります。

  • srcbufへの pragma dataflag 16 適用: dosrcline関数内で使用される一時的なバッファstatic byte srcbuf[1000];#pragma dataflag 16でマークされました。これは、このバッファが一時的な文字列データを保持するだけで、ポインタを含まないため、GCスキャンから除外されるべきであることを示しています。

  • nelem(files) から maxfiles への変更: 静的配列filesのサイズを取得するために使用されていたnelem(files)(要素数を返すマクロ)が、動的割り当てになったため、代わりに定数maxfilesを使用するように変更されました。

これらの変更は、シンボルテーブルの処理におけるメモリ管理をより効率的にし、GCの負担を軽減することを目的としています。特に、filesを動的に割り当てることで、シンボルテーブルのサイズが固定されることによる潜在的な問題(例えば、200以上のファイルを持つ大規模なプログラムでの問題)を回避し、よりスケーラブルな設計になっています。

関連リンク

参考にした情報源リンク