[インデックス 13214] ファイルの概要
このコミットは、Go言語のランタイムにおけるガベージコレクタ(GC)の挙動を最適化することを目的としています。具体的には、シンボルテーブルがGCの対象とならないように変更を加えることで、GCの効率を向上させ、パフォーマンスへの影響を軽減します。シンボルテーブルは、プログラムの実行に必要なメタデータ(関数名、ファイル名、行番号など)を格納する領域であり、通常は静的なデータとして扱われます。このコミットでは、シンボルテーブルのメモリ割り当て方法を変更し、GCが不要なスキャンを行わないようにすることで、GCサイクル中のオーバーヘッドを削減しています。
コミット
commit 46d7d5fcf57f31afa62b23ac379a140e69f4753e
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Wed May 30 13:04:48 2012 -0400
runtime: hide symbol table from garbage collector
R=rsc
CC=golang-dev
https://golang.org/cl/6243059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/46d7d5fcf57f31afa62b23ac379a140e69f4753e
元コミット内容
runtime: hide symbol table from garbage collector
R=rsc
CC=golang-dev
https://golang.org/cl/6243059
変更の背景
Go言語のランタイムは、プログラムの実行を管理し、メモリ管理(ガベージコレクションを含む)やスケジューリングなどの低レベルなタスクを処理します。ガベージコレクタは、不要になったメモリを自動的に解放することで、メモリリークを防ぎ、開発者が手動でメモリを管理する負担を軽減します。しかし、GCは実行時にプログラムのメモリをスキャンし、到達可能なオブジェクトを特定する必要があります。このスキャンプロセスは、特に大規模なアプリケーションやメモリ使用量が多い場合に、パフォーマンスのボトルネックとなる可能性があります。
シンボルテーブルは、コンパイル時に生成されるメタデータであり、実行時にはその内容が変化することはほとんどありません。これには、関数名、ソースファイル名、行番号などの情報が含まれており、デバッグやプロファイリングの際に利用されます。従来のGoランタイムでは、このシンボルテーブルもGCの対象となっており、GCサイクルごとにスキャンされていました。しかし、シンボルテーブルは静的なデータであり、ヒープ上の動的に割り当てられたオブジェクトへのポインタを含まないため、GCがスキャンする必要はありません。
このコミットの背景には、シンボルテーブルをGCの対象から除外することで、GCのスキャン範囲を縮小し、GCの実行時間を短縮するという目的があります。これにより、Goプログラム全体のパフォーマンスが向上し、特にGCの頻度が高いアプリケーションにおいて、よりスムーズな実行が期待されます。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理する非常に重要なコンポーネントです。C言語で記述されており、GoプログラムがOS上で動作するために必要な低レベルな機能を提供します。これには、以下のような主要な機能が含まれます。
- ガベージコレクション (Garbage Collection, GC): 不要になったメモリを自動的に解放する機能。GoのGCは並行かつ低遅延で動作するように設計されています。
- ゴルーチン (Goroutines): Goの軽量な並行処理単位。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。
- スケジューラ (Scheduler): ゴルーチンをOSスレッドにマッピングし、効率的に実行するためのスケジューリングを行います。
- メモリ管理: ヒープメモリの割り当てと解放を管理します。
ガベージコレクション (Garbage Collection, GC)
ガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはや参照されなくなった(到達不可能になった)メモリ領域を自動的に特定し、解放するプロセスです。これにより、開発者は手動でのメモリ管理(malloc
やfree
など)から解放され、メモリリークのリスクを低減できます。
GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。
- マークフェーズ: GCは、プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。このプロセスでは、オブジェクト内のポインタをたどって、さらに到達可能なオブジェクトをマークしていきます。
- スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが、不要なメモリとして識別され、解放されます。
GCのパフォーマンスは、マークフェーズでスキャンするメモリ領域の量に大きく依存します。スキャンするデータが多ければ多いほど、GCの実行時間は長くなり、プログラムの実行が一時停止する「ストップ・ザ・ワールド(STW)」時間が長くなる可能性があります。
シンボルテーブル (Symbol Table)
シンボルテーブルは、コンパイラやリンカによって生成されるデータ構造で、プログラム内のシンボル(変数名、関数名、ファイル名、行番号など)とそのアドレスや型情報などの関連情報をマッピングします。実行時には、デバッガがシンボルテーブルを利用して、ソースコードの行番号と実行中のマシンコードのアドレスを関連付けたり、関数名を表示したりします。
Goのバイナリには、pclntab
(PC-line table) と呼ばれるテーブルが含まれており、これはプログラムカウンタ(PC)とソースコードの行番号、ファイル名を関連付けるための情報を持っています。このテーブルは、デバッグ情報やスタックトレースの生成に不可欠です。シンボルテーブルは、通常、プログラムの実行中に内容が変更されることはなく、静的なデータとして扱われます。
mallocgc
と FlagNoPointers
Goランタイムには、メモリを割り当てるための関数がいくつか存在します。runtime·mallocgc
は、ガベージコレクタによって管理されるヒープメモリを割り当てるための関数です。この関数は、割り当てるメモリのサイズだけでなく、そのメモリ領域がポインタを含むかどうかを示すフラグを受け取ることができます。
FlagNoPointers
は、runtime·mallocgc
に渡されるフラグの一つで、割り当てられるメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。GCは、このフラグが設定されたメモリ領域をスキャンする必要がないと判断するため、GCの効率が向上します。これは、シンボルテーブルのような静的なデータや、ポインタを含まない純粋なデータ構造を割り当てる際に特に有用です。
技術的詳細
このコミットの核心は、Goランタイムがシンボルテーブル(特にファイル名や関数名などの文字列データ)を格納するために使用するメモリ領域を、ガベージコレクタがスキャンしないように変更することです。
変更前は、シンボルテーブルに関連する文字列データが runtime·gostring
を介して割り当てられており、これはGCの管理下にありました。そのため、GCサイクルごとにこれらの文字列データもスキャンされ、GCのオーバーヘッドの一因となっていました。
このコミットでは、以下の主要な変更が導入されています。
-
hugestring
の導入:static String hugestring;
とstatic int32 hugestring_len;
がsrc/pkg/runtime/symtab.c
に追加されました。hugestring
は、シンボルテーブル内のすべての文字列(ファイルパスなど)を連続して格納するための単一の大きなメモリブロックとして機能します。- これにより、個々の文字列が小さな独立したオブジェクトとしてGCに認識されるのではなく、
hugestring
全体がGCから「隠蔽」される対象となります。
-
gostringn
関数の追加:gostringn(byte *p, int32 l)
という新しい関数が追加されました。- この関数は、指定されたバイト列
p
と長さl
をhugestring
にコピーし、その部分文字列を表すString
型の値を返します。 - 重要なのは、
gostringn
がhugestring
の内部に文字列を格納し、そのhugestring
自体がGCの対象外となるように設計されている点です。
-
mallocgc
とFlagNoPointers
の利用:buildfuncs
関数内で、func
とfname
テーブルのメモリ割り当てにruntime·mallocgc
が使用されるようになりました。- 特に重要なのは、これらの割り当てに
FlagNoPointers
フラグが渡されている点です。func = runtime·mallocgc((nfunc+1)*sizeof func[0], FlagNoPointers, 0, 1); fname = runtime·mallocgc(nfname*sizeof fname[0], FlagNoPointers, 0, 1);
- このフラグは、これらのメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。シンボルテーブルのデータは、主に文字列や数値などの静的な情報であり、ヒープ上の他のGC管理オブジェクトへのポインタを含まないため、このフラグを安全に設定できます。
hugestring
自体も、dosrcline
の2回目のパスでruntime·mallocgc(hugestring_len, FlagNoPointers, 0, 0)
を使って割り当てられています。これにより、hugestring
全体がGCの対象から外れます。
-
シンボルテーブル構築の2パス処理:
buildfuncs
関数内で、walksymtab(dosrcline)
が2回呼び出されるようになりました。- パス1:
hugestring_len
を計算するためにシンボルテーブルをウォークします。この時点ではhugestring.str
はnil
です。 - パス2:
hugestring_len
で確保されたhugestring
に実際の文字列データを格納し、gostringn
を使用して文字列スライスを作成します。
- パス1:
- この2パス処理により、必要な
hugestring
の正確なサイズを事前に決定し、一度に連続したメモリブロックを割り当てることが可能になります。
これらの変更により、シンボルテーブルに関連するデータは、GCがスキャンする必要のないメモリ領域に配置されるようになります。これにより、GCはより少ないメモリ領域をスキャンするだけで済み、GCサイクルが短縮され、Goプログラム全体のパフォーマンスが向上します。
コアとなるコードの変更箇所
src/pkg/runtime/symtab.c
における主要な変更点は以下の通りです。
-
malloc.h
のインクルード:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -16,6 +16,7 @@ #include "defs_GOOS_GOARCH.h" #include "os_GOOS.h" #include "arch_GOARCH.h" ++#include "malloc.h"
runtime·mallocgc
を使用するために、メモリ割り当て関連の定義が含まれるmalloc.h
がインクルードされました。 -
hugestring
の定義:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -28,6 +29,11 @@ struct Sym // byte *gotype; }; +// A dynamically allocated string containing multiple substrings. +// Individual strings are slices of hugestring. +static String hugestring; +static int32 hugestring_len; ++
シンボルテーブルの文字列を格納するための
hugestring
変数と、その長さを追跡するためのhugestring_len
が追加されました。 -
makepath
の戻り値の変更:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -135,14 +141,15 @@ dofunc(Sym *sym)\n // put together the path name for a z entry.\n // the f entries have been accumulated into fname already.\n -static void\n- // returns the length of the path name.\n-+static int32\n makepath(byte *buf, int32 nbuf, byte *path)\n {\n int32 n, len;\n byte *p, *ep, *q;\n \n if(nbuf <= 0)\n -\t\treturn;\n+\t\treturn 0;\n \n p = buf;\
makepath
関数がvoid
からint32
を返すように変更され、生成されたパス名の長さを返すようになりました。これはgostringn
で使用されます。 -
gostringn
関数の追加:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -163,6 +170,26 @@ makepath(byte *buf, int32 nbuf, byte *path)\n \truntime·memmove(p, q, len+1);\n \tp += len;\n }\n+\treturn p - buf;\n+}\n+\n+// appends p to hugestring\n+static String\n+gostringn(byte *p, int32 l)\n+{\n+\tString s;\n+\n+\tif(l == 0)\n+\t\treturn runtime·emptystring;\n+\tif(hugestring.str == nil) {\n+\t\thugestring_len += l;\n+\t\treturn runtime·emptystring;\n+\t}\n+\ts.str = hugestring.str + hugestring.len;\n+\ts.len = l;\n+\thugestring.len += s.len;\n+\truntime·memmove(s.str, p, l);\n+\treturn s;\n }\
hugestring
に文字列を効率的に追加し、その部分文字列を表すString
を返すgostringn
関数が追加されました。 -
dosrcline
でのgostringn
の使用:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -200,23 +229,23 @@ dosrcline(Sym *sym)\n case \'z\':\n \tif(sym->value == 1) {\n \t\t// entry for main source file for a new object.\n -\t\t\tmakepath(srcbuf, sizeof srcbuf, sym->name+1);\n +\t\t\tl = makepath(srcbuf, sizeof srcbuf, sym->name+1);\n \t\tnhist = 0;\n \t\tnfile = 0;\n \t\tif(nfile == nelem(files))\n \t\t\treturn;\n -\t\t\tfiles[nfile].srcstring = runtime·gostring(srcbuf);\n +\t\t\tfiles[nfile].srcstring = gostringn(srcbuf, l);\n \t\tfiles[nfile].aline = 0;\n \t\tfiles[nfile++].delta = 0;\n \t} else {\n \t\t// push or pop of included file.\n -\t\t\tmakepath(srcbuf, sizeof srcbuf, sym->name+1);\n +\t\t\tl = makepath(srcbuf, sizeof srcbuf, sym->name+1);\n \t\tif(srcbuf[0] != \'\\0\') {\n \t\t\tif(nhist++ == 0)\n \t\t\t\tincstart = sym->value;\n \t\t\tif(nhist == 0 && nfile < nelem(files)) {\n \t\t\t\t// new top-level file\n -\t\t\t\t\tfiles[nfile].srcstring = runtime·gostring(srcbuf);\n +\t\t\t\t\tfiles[nfile].srcstring = gostringn(srcbuf, l);\n \t\t\t\tfiles[nfile].aline = sym->value;\
dosrcline
関数内で、ファイルパスの文字列をruntime·gostring
の代わりに新しく追加されたgostringn
を使って割り当てるように変更されました。 -
buildfuncs
でのmallocgc
と2パス処理:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -408,10 +437,12 @@ buildfuncs(void)\n \tnfname = 0;\n \twalksymtab(dofunc);\n \n -\t// initialize tables\n -\tfunc = runtime·mal((nfunc+1)*sizeof func[0]);\n +\t// Initialize tables.\n +\t// Can use FlagNoPointers - all pointers either point into sections of the executable\n +\t// or point into hugestring.\n +\tfunc = runtime·mallocgc((nfunc+1)*sizeof func[0], FlagNoPointers, 0, 1);\n func[nfunc].entry = (uint64)etext;\n -\tfname = runtime·mal(nfname*sizeof fname[0]);\n +\tfname = runtime·mallocgc(nfname*sizeof fname[0], FlagNoPointers, 0, 1);\n \tnfunc = 0;\n \twalksymtab(dofunc);\n \n @@ -419,7 +450,13 @@ buildfuncs(void)\n \tsplitpcln();\n \n // record src file and line info for each func\n -\twalksymtab(dosrcline);\n +\twalksymtab(dosrcline); // pass 1: determine hugestring_len\n +\thugestring.str = runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0);\n +\thugestring.len = 0;\n +\twalksymtab(dosrcline); // pass 2: fill and use hugestring\n +\n +\tif(hugestring.len != hugestring_len)\n +\t\truntime·throw(\"buildfunc: problem in initialization procedure\");\
func
とfname
の割り当てにruntime·mallocgc
とFlagNoPointers
が使用されるようになりました。また、dosrcline
を2回呼び出すことで、hugestring
のサイズを決定し、その後に実際の文字列データを格納する2パス処理が導入されました。
コアとなるコードの解説
このコミットの主要な目的は、Goランタイムのシンボルテーブルがガベージコレクタによってスキャンされないようにすることです。これを実現するために、以下のメカニズムが導入されています。
-
hugestring
による文字列の一元管理とGCからの除外:- Goのシンボルテーブルには、ソースファイル名や関数名などの多くの文字列が含まれています。これらの文字列が個別にGC管理下のヒープに割り当てられていると、GCはそれらすべてをスキャンする必要があります。
hugestring
は、これらのすべての文字列を格納するための単一の大きな連続したメモリブロックとして機能します。- この
hugestring
自体は、runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0)
を使用して割り当てられます。FlagNoPointers
フラグは、このメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。これにより、hugestring
全体がGCの対象から外れ、GCはシンボルテーブルの文字列データをスキャンする必要がなくなります。
-
gostringn
による効率的な文字列スライス生成:gostringn
関数は、hugestring
内の特定の部分を指すString
型の値を生成します。これは、Goの文字列が内部的にポインタと長さのペアとして表現されることを利用しています。gostringn
は、新しいメモリを割り当てるのではなく、既存のhugestring
内のオフセットと長さを指定することで、文字列スライスを作成します。これにより、メモリコピーのオーバーヘッドが削減され、効率的な文字列管理が可能になります。
-
2パス処理によるメモリ割り当ての最適化:
buildfuncs
関数におけるdosrcline
の2回呼び出しは、hugestring
のメモリ割り当てを最適化するための重要なステップです。- 1回目のパス:
hugestring.str
がnil
の状態でdosrcline
を実行することで、gostringn
は実際の文字列コピーを行わず、必要なhugestring_len
(全文字列の合計長)を計算します。 - 2回目のパス: 1回目のパスで計算された
hugestring_len
を使用して、hugestring
のための正確なサイズのメモリブロックがruntime·mallocgc
とFlagNoPointers
で割り当てられます。その後、2回目のdosrcline
実行で、実際の文字列データがこのhugestring
にコピーされます。 - このアプローチにより、必要なメモリ量を正確に把握し、一度に連続したメモリブロックを割り当てることができ、メモリの断片化を防ぎ、キャッシュ効率を向上させます。
-
func
およびfname
テーブルのGCからの除外:func
(関数情報) とfname
(ファイル名情報) のテーブルも、runtime·mallocgc
とFlagNoPointers
を使用して割り当てられるようになりました。- これらのテーブルは、シンボルテーブルと同様に、主に静的なデータ(関数エントリポイント、ファイル名文字列へのポインタなど)を含み、GCがスキャンすべきヒープ上のポインタは含まないため、GCの対象から除外することが適切です。
これらの変更により、Goランタイムはシンボルテーブルに関連するメモリをGCの管理外に置くことで、GCのスキャン範囲を大幅に縮小し、GCの実行効率を向上させています。これは、Goプログラムの全体的なパフォーマンス、特にGCのオーバーヘッドが問題となるようなアプリケーションにおいて、顕著な改善をもたらします。
関連リンク
- Go Change List (CL): https://golang.org/cl/6243059 このコミットに対応するGoの変更リストページです。詳細な議論やレビューコメント、関連する変更履歴を確認できます。
参考にした情報源リンク
- Go Runtime Source Code:
src/pkg/runtime/symtab.c
(Goのソースコードリポジトリ内) - Go Garbage Collection Documentation: Goの公式ドキュメントやブログ記事で、GCの仕組みや進化について解説されているもの。
- 例: https://go.dev/blog/go15gc (Go 1.5のGCに関する記事ですが、GCの基本的な概念理解に役立ちます)
- Go Memory Management: Goのメモリ管理に関する技術記事や解説。
- 例: https://go.dev/doc/effective_go#allocation_with_make (Goのメモリ割り当ての基本的な概念)
- Compiler Symbol Tables: コンパイラにおけるシンボルテーブルの一般的な概念に関する情報。
- 例: Wikipediaの「シンボルテーブル」の項目など。# [インデックス 13214] ファイルの概要
このコミットは、Go言語のランタイムにおけるガベージコレクタ(GC)の挙動を最適化することを目的としています。具体的には、シンボルテーブルがGCの対象とならないように変更を加えることで、GCの効率を向上させ、パフォーマンスへの影響を軽減します。シンボルテーブルは、プログラムの実行に必要なメタデータ(関数名、ファイル名、行番号など)を格納する領域であり、通常は静的なデータとして扱われます。このコミットでは、シンボルテーブルのメモリ割り当て方法を変更し、GCが不要なスキャンを行わないようにすることで、GCサイクル中のオーバーヘッドを削減しています。
コミット
commit 46d7d5fcf57f31afa62b23ac379a140e69f4753e
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Wed May 30 13:04:48 2012 -0400
runtime: hide symbol table from garbage collector
R=rsc
CC=golang-dev
https://golang.org/cl/6243059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/46d7d5fcf57f31afa62b23ac379a140e69f4753e
元コミット内容
runtime: hide symbol table from garbage collector
R=rsc
CC=golang-dev
https://golang.org/cl/6243059
変更の背景
Go言語のランタイムは、プログラムの実行を管理し、メモリ管理(ガベージコレクションを含む)やスケジューリングなどの低レベルなタスクを処理します。C言語で記述されており、GoプログラムがOS上で動作するために必要な低レベルな機能を提供します。これには、以下のような主要な機能が含まれます。
- ガベージコレクション (Garbage Collection, GC): 不要になったメモリを自動的に解放する機能。GoのGCは並行かつ低遅延で動作するように設計されています。
- ゴルーチン (Goroutines): Goの軽量な並行処理単位。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。
- スケジューラ (Scheduler): ゴルーチンをOSスレッドにマッピングし、効率的に実行するためのスケジューリングを行います。
- メモリ管理: ヒープメモリの割り当てと解放を管理します。
ガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはや参照されなくなった(到達不可能になった)メモリ領域を自動的に特定し、解放するプロセスです。これにより、開発者は手動でのメモリ管理(malloc
やfree
など)から解放され、メモリリークのリスクを低減できます。
GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。
- マークフェーズ: GCは、プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。このプロセスでは、オブジェクト内のポインタをたどって、さらに到達可能なオブジェクトをマークしていきます。
- スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが、不要なメモリとして識別され、解放されます。
GCのパフォーマンスは、マークフェーズでスキャンするメモリ領域の量に大きく依存します。スキャンするデータが多ければ多いほど、GCの実行時間は長くなり、プログラムの実行が一時停止する「ストップ・ザ・ワールド(STW)」時間が長くなる可能性があります。
シンボルテーブルは、コンパイル時に生成されるメタデータであり、実行時にはその内容が変化することはほとんどありません。これには、関数名、ソースファイル名、行番号などの情報が含まれており、デバッグやプロファイリングの際に利用されます。従来のGoランタイムでは、このシンボルテーブルもGCの対象となっており、GCサイクルごとにスキャンされていました。しかし、シンボルテーブルは静的なデータであり、ヒープ上の動的に割り当てられたオブジェクトへのポインタを含まないため、GCがスキャンする必要はありません。
このコミットの背景には、シンボルテーブルをGCの対象から除外することで、GCのスキャン範囲を縮小し、GCの実行時間を短縮するという目的があります。これにより、Goプログラム全体のパフォーマンスが向上し、特にGCの頻度が高いアプリケーションにおいて、よりスムーズな実行が期待されます。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理する非常に重要なコンポーネントです。C言語で記述されており、GoプログラムがOS上で動作するために必要な低レベルな機能を提供します。これには、以下のような主要な機能が含まれます。
- ガベージコレクション (Garbage Collection, GC): 不要になったメモリを自動的に解放する機能。GoのGCは並行かつ低遅延で動作するように設計されています。
- ゴルーチン (Goroutines): Goの軽量な並行処理単位。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。
- スケジューラ (Scheduler): ゴルーチンをOSスレッドにマッピングし、効率的に実行するためのスケジューリングを行います。
- メモリ管理: ヒープメモリの割り当てと解放を管理します。
ガベージコレクション (Garbage Collection, GC)
ガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはや参照されなくなった(到達不可能になった)メモリ領域を自動的に特定し、解放するプロセスです。これにより、開発者は手動でのメモリ管理(malloc
やfree
など)から解放され、メモリリークのリスクを低減できます。
GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。
- マークフェーズ: GCは、プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。このプロセスでは、オブジェクト内のポインタをたどって、さらに到達可能なオブジェクトをマークしていきます。
- スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが、不要なメモリとして識別され、解放されます。
GCのパフォーマンスは、マークフェーズでスキャンするメモリ領域の量に大きく依存します。スキャンするデータが多ければ多いほど、GCの実行時間は長くなり、プログラムの実行が一時停止する「ストップ・ザ・ワールド(STW)」時間が長くなる可能性があります。
シンボルテーブル (Symbol Table)
シンボルテーブルは、コンパイラやリンカによって生成されるデータ構造で、プログラム内のシンボル(変数名、関数名、ファイル名、行番号など)とそのアドレスや型情報などの関連情報をマッピングします。実行時には、デバッガがシンボルテーブルを利用して、ソースコードの行番号と実行中のマシンコードのアドレスを関連付けたり、関数名を表示したりします。
Goのバイナリには、pclntab
(PC-line table) と呼ばれるテーブルが含まれており、これはプログラムカウンタ(PC)とソースコードの行番号、ファイル名を関連付けるための情報を持っています。このテーブルは、デバッグ情報やスタックトレースの生成に不可欠です。シンボルテーブルは、通常、プログラムの実行中に内容が変更されることはなく、静的なデータとして扱われます。
mallocgc
と FlagNoPointers
Goランタイムには、メモリを割り当てるための関数がいくつか存在します。runtime·mallocgc
は、ガベージコレクタによって管理されるヒープメモリを割り当てるための関数です。この関数は、割り当てるメモリのサイズだけでなく、そのメモリ領域がポインタを含むかどうかを示すフラグを受け取ることができます。
FlagNoPointers
は、runtime·mallocgc
に渡されるフラグの一つで、割り当てられるメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。GCは、このフラグが設定されたメモリ領域をスキャンする必要がないと判断するため、GCの効率が向上します。これは、シンボルテーブルのような静的なデータや、ポインタを含まない純粋なデータ構造を割り当てる際に特に有用です。
技術的詳細
このコミットの核心は、Goランタイムがシンボルテーブル(特にファイル名や関数名などの文字列データ)を格納するために使用するメモリ領域を、ガベージコレクタがスキャンしないように変更することです。
変更前は、シンボルテーブルに関連する文字列データが runtime·gostring
を介して割り当てられており、これはGCの管理下にありました。そのため、GCサイクルごとにこれらの文字列データもスキャンされ、GCのオーバーヘッドの一因となっていました。
このコミットでは、以下の主要な変更が導入されています。
-
hugestring
の導入:static String hugestring;
とstatic int32 hugestring_len;
がsrc/pkg/runtime/symtab.c
に追加されました。hugestring
は、シンボルテーブル内のすべての文字列(ファイルパスなど)を連続して格納するための単一の大きなメモリブロックとして機能します。- これにより、個々の文字列が小さな独立したオブジェクトとしてGCに認識されるのではなく、
hugestring
全体がGCから「隠蔽」される対象となります。
-
gostringn
関数の追加:gostringn(byte *p, int32 l)
という新しい関数が追加されました。- この関数は、指定されたバイト列
p
と長さl
をhugestring
にコピーし、その部分文字列を表すString
型の値を返します。 - 重要なのは、
gostringn
がhugestring
の内部に文字列を格納し、そのhugestring
自体がGCの対象外となるように設計されている点です。
-
mallocgc
とFlagNoPointers
の利用:buildfuncs
関数内で、func
とfname
テーブルのメモリ割り当てにruntime·mallocgc
が使用されるようになりました。- 特に重要なのは、これらの割り当てに
FlagNoPointers
フラグが渡されている点です。func = runtime·mallocgc((nfunc+1)*sizeof func[0], FlagNoPointers, 0, 1); fname = runtime·mallocgc(nfname*sizeof fname[0], FlagNoPointers, 0, 1);
- このフラグは、これらのメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。シンボルテーブルのデータは、主に文字列や数値などの静的な情報であり、ヒープ上の他のGC管理オブジェクトへのポインタを含まないため、このフラグを安全に設定できます。
hugestring
自体も、dosrcline
の2回目のパスでruntime·mallocgc(hugestring_len, FlagNoPointers, 0, 0)
を使って割り当てられています。これにより、hugestring
全体がGCの対象から外れます。
-
シンボルテーブル構築の2パス処理:
buildfuncs
関数内で、walksymtab(dosrcline)
が2回呼び出されるようになりました。- パス1:
hugestring_len
を計算するためにシンボルテーブルをウォークします。この時点ではhugestring.str
はnil
です。 - パス2:
hugestring_len
で確保されたhugestring
に実際の文字列データを格納し、gostringn
を使用して文字列スライスを作成します。
- パス1:
- この2パス処理により、必要な
hugestring
の正確なサイズを事前に決定し、一度に連続したメモリブロックを割り当てることが可能になります。
これらの変更により、シンボルテーブルに関連するデータは、GCがスキャンする必要のないメモリ領域に配置されるようになります。これにより、GCはより少ないメモリ領域をスキャンするだけで済み、GCサイクルが短縮され、Goプログラム全体のパフォーマンスが向上します。
コアとなるコードの変更箇所
src/pkg/runtime/symtab.c
における主要な変更点は以下の通りです。
-
malloc.h
のインクルード:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -16,6 +16,7 @@ #include "defs_GOOS_GOARCH.h" #include "os_GOOS.h" #include "arch_GOARCH.h" ++#include "malloc.h"
runtime·mallocgc
を使用するために、メモリ割り当て関連の定義が含まれるmalloc.h
がインクルードされました。 -
hugestring
の定義:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -28,6 +29,11 @@ struct Sym // byte *gotype; }; +// A dynamically allocated string containing multiple substrings. +// Individual strings are slices of hugestring. +static String hugestring; +static int32 hugestring_len; ++
シンボルテーブルの文字列を格納するための
hugestring
変数と、その長さを追跡するためのhugestring_len
が追加されました。 -
makepath
の戻り値の変更:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -135,14 +141,15 @@ dofunc(Sym *sym)\n // put together the path name for a z entry.\n // the f entries have been accumulated into fname already.\n -static void\n- // returns the length of the path name.\n-+static int32\n makepath(byte *buf, int32 nbuf, byte *path)\n {\n int32 n, len;\n byte *p, *ep, *q;\n \n if(nbuf <= 0)\n -\t\treturn;\n+\t\treturn 0;\n \n p = buf;\
makepath
関数がvoid
からint32
を返すように変更され、生成されたパス名の長さを返すようになりました。これはgostringn
で使用されます。 -
gostringn
関数の追加:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -163,6 +170,26 @@ makepath(byte *buf, int32 nbuf, byte *path)\n \truntime·memmove(p, q, len+1);\n \tp += len;\n }\n+\treturn p - buf;\n+}\n+\n+// appends p to hugestring\n+static String\n+gostringn(byte *p, int32 l)\n+{\n+\tString s;\n+\n+\tif(l == 0)\n+\t\treturn runtime·emptystring;\n+\tif(hugestring.str == nil) {\n+\t\thugestring_len += l;\n+\t\treturn runtime·emptystring;\n+\t}\n+\ts.str = hugestring.str + hugestring.len;\n+\ts.len = l;\n+\thugestring.len += s.len;\n+\truntime·memmove(s.str, p, l);\n+\treturn s;\n }\
hugestring
に文字列を効率的に追加し、その部分文字列を表すString
を返すgostringn
関数が追加されました。 -
dosrcline
でのgostringn
の使用:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -200,23 +229,23 @@ dosrcline(Sym *sym)\n case \'z\':\n \tif(sym->value == 1) {\n \t\t// entry for main source file for a new object.\n -\t\t\tmakepath(srcbuf, sizeof srcbuf, sym->name+1);\n +\t\t\tl = makepath(srcbuf, sizeof srcbuf, sym->name+1);\n \t\tnhist = 0;\n \t\tnfile = 0;\n \t\tif(nfile == nelem(files))\n \t\t\treturn;\n -\t\t\tfiles[nfile].srcstring = runtime·gostring(srcbuf);\n +\t\t\tfiles[nfile].srcstring = gostringn(srcbuf, l);\n \t\tfiles[nfile].aline = 0;\n \t\tfiles[nfile++].delta = 0;\n \t} else {\n \t\t// push or pop of included file.\n -\t\t\tmakepath(srcbuf, sizeof srcbuf, sym->name+1);\n +\t\t\tl = makepath(srcbuf, sizeof srcbuf, sym->name+1);\n \t\tif(srcbuf[0] != \'\\0\') {\n \t\t\tif(nhist++ == 0)\n \t\t\t\tincstart = sym->value;\n \t\t\tif(nhist == 0 && nfile < nelem(files)) {\n \t\t\t\t// new top-level file\n -\t\t\t\t\tfiles[nfile].srcstring = runtime·gostring(srcbuf);\n +\t\t\t\t\tfiles[nfile].srcstring = gostringn(srcbuf, l);\n \t\t\t\tfiles[nfile].aline = sym->value;\
dosrcline
関数内で、ファイルパスの文字列をruntime·gostring
の代わりに新しく追加されたgostringn
を使って割り当てるように変更されました。 -
buildfuncs
でのmallocgc
と2パス処理:--- a/src/pkg/runtime/symtab.c +++ b/src/pkg/runtime/symtab.c @@ -408,10 +437,12 @@ buildfuncs(void)\n \tnfname = 0;\n \twalksymtab(dofunc);\n \n -\t// initialize tables\n -\tfunc = runtime·mal((nfunc+1)*sizeof func[0]);\n +\t// Initialize tables.\n +\t// Can use FlagNoPointers - all pointers either point into sections of the executable\n +\t// or point into hugestring.\n +\tfunc = runtime·mallocgc((nfunc+1)*sizeof func[0], FlagNoPointers, 0, 1);\n func[nfunc].entry = (uint64)etext;\n -\tfname = runtime·mal(nfname*sizeof fname[0]);\n +\tfname = runtime·mallocgc(nfname*sizeof fname[0], FlagNoPointers, 0, 1);\n \tnfunc = 0;\n \twalksymtab(dofunc);\n \n @@ -419,7 +450,13 @@ buildfuncs(void)\n \tsplitpcln();\n \n // record src file and line info for each func\n -\twalksymtab(dosrcline);\n +\twalksymtab(dosrcline); // pass 1: determine hugestring_len\n +\thugestring.str = runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0);\n +\thugestring.len = 0;\n +\twalksymtab(dosrcline); // pass 2: fill and use hugestring\n +\n +\tif(hugestring.len != hugestring_len)\n +\t\truntime·throw(\"buildfunc: problem in initialization procedure\");\
func
とfname
の割り当てにruntime·mallocgc
とFlagNoPointers
が使用されるようになりました。また、dosrcline
を2回呼び出すことで、hugestring
のサイズを決定し、その後に実際の文字列データを格納する2パス処理が導入されました。
コアとなるコードの解説
このコミットの主要な目的は、Goランタイムのシンボルテーブルがガベージコレクタによってスキャンされないようにすることです。これを実現するために、以下のメカニズムが導入されています。
-
hugestring
による文字列の一元管理とGCからの除外:- Goのシンボルテーブルには、ソースファイル名や関数名などの多くの文字列が含まれています。これらの文字列が個別にGC管理下のヒープに割り当てられていると、GCはそれらすべてをスキャンする必要があります。
hugestring
は、これらのすべての文字列を格納するための単一の大きな連続したメモリブロックとして機能します。- この
hugestring
自体は、runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0)
を使用して割り当てられます。FlagNoPointers
フラグは、このメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。これにより、hugestring
全体がGCの対象から外れ、GCはシンボルテーブルの文字列データをスキャンする必要がなくなります。
-
gostringn
による効率的な文字列スライス生成:gostringn
関数は、hugestring
内の特定の部分を指すString
型の値を生成します。これは、Goの文字列が内部的にポインタと長さのペアとして表現されることを利用しています。gostringn
は、新しいメモリを割り当てるのではなく、既存のhugestring
内のオフセットと長さを指定することで、文字列スライスを作成します。これにより、メモリコピーのオーバーヘッドが削減され、効率的な文字列管理が可能になります。
-
2パス処理によるメモリ割り当ての最適化:
buildfuncs
関数におけるdosrcline
の2回呼び出しは、hugestring
のメモリ割り当てを最適化するための重要なステップです。- 1回目のパス:
hugestring.str
がnil
の状態でdosrcline
を実行することで、gostringn
は実際の文字列コピーを行わず、必要なhugestring_len
(全文字列の合計長)を計算します。 - 2回目のパス: 1回目のパスで計算された
hugestring_len
を使用して、hugestring
のための正確なサイズのメモリブロックがruntime·mallocgc
とFlagNoPointers
で割り当てられます。その後、2回目のdosrcline
実行で、実際の文字列データがこのhugestring
にコピーされます。 - このアプローチにより、必要なメモリ量を正確に把握し、一度に連続したメモリブロックを割り当てることができ、メモリの断片化を防ぎ、キャッシュ効率を向上させます。
-
func
およびfname
テーブルのGCからの除外:func
(関数情報) とfname
(ファイル名情報) のテーブルも、runtime·mallocgc
とFlagNoPointers
を使用して割り当てられるようになりました。- これらのテーブルは、シンボルテーブルと同様に、主に静的なデータ(関数エントリポイント、ファイル名文字列へのポインタなど)を含み、GCがスキャンすべきヒープ上のポインタは含まないため、GCの対象から除外することが適切です。
これらの変更により、Goランタイムはシンボルテーブルに関連するメモリをGCの管理外に置くことで、GCのスキャン範囲を大幅に縮小し、GCの実行効率を向上させています。これは、Goプログラムの全体的なパフォーマンス、特にGCのオーバーヘッドが問題となるようなアプリケーションにおいて、顕著な改善をもたらします。
関連リンク
- Go Change List (CL): https://golang.org/cl/6243059 このコミットに対応するGoの変更リストページです。詳細な議論やレビューコメント、関連する変更履歴を確認できます。
参考にした情報源リンク
- Go Runtime Source Code:
src/pkg/runtime/symtab.c
(Goのソースコードリポジトリ内) - Go Garbage Collection Documentation: Goの公式ドキュメントやブログ記事で、GCの仕組みや進化について解説されているもの。
- 例: https://go.dev/blog/go15gc (Go 1.5のGCに関する記事ですが、GCの基本的な概念理解に役立ちます)
- Go Memory Management: Goのメモリ管理に関する技術記事や解説。
- 例: https://go.dev/doc/effective_go#allocation_with_make (Goのメモリ割り当ての基本的な概念)
- Compiler Symbol Tables: コンパイラにおけるシンボルテーブルの一般的な概念に関する情報。
- 例: Wikipediaの「シンボルテーブル」の項目など。