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

[インデックス 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)

ガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはや参照されなくなった(到達不可能になった)メモリ領域を自動的に特定し、解放するプロセスです。これにより、開発者は手動でのメモリ管理(mallocfreeなど)から解放され、メモリリークのリスクを低減できます。

GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。

  1. マークフェーズ: GCは、プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。このプロセスでは、オブジェクト内のポインタをたどって、さらに到達可能なオブジェクトをマークしていきます。
  2. スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが、不要なメモリとして識別され、解放されます。

GCのパフォーマンスは、マークフェーズでスキャンするメモリ領域の量に大きく依存します。スキャンするデータが多ければ多いほど、GCの実行時間は長くなり、プログラムの実行が一時停止する「ストップ・ザ・ワールド(STW)」時間が長くなる可能性があります。

シンボルテーブル (Symbol Table)

シンボルテーブルは、コンパイラやリンカによって生成されるデータ構造で、プログラム内のシンボル(変数名、関数名、ファイル名、行番号など)とそのアドレスや型情報などの関連情報をマッピングします。実行時には、デバッガがシンボルテーブルを利用して、ソースコードの行番号と実行中のマシンコードのアドレスを関連付けたり、関数名を表示したりします。

Goのバイナリには、pclntab (PC-line table) と呼ばれるテーブルが含まれており、これはプログラムカウンタ(PC)とソースコードの行番号、ファイル名を関連付けるための情報を持っています。このテーブルは、デバッグ情報やスタックトレースの生成に不可欠です。シンボルテーブルは、通常、プログラムの実行中に内容が変更されることはなく、静的なデータとして扱われます。

mallocgcFlagNoPointers

Goランタイムには、メモリを割り当てるための関数がいくつか存在します。runtime·mallocgc は、ガベージコレクタによって管理されるヒープメモリを割り当てるための関数です。この関数は、割り当てるメモリのサイズだけでなく、そのメモリ領域がポインタを含むかどうかを示すフラグを受け取ることができます。

FlagNoPointers は、runtime·mallocgc に渡されるフラグの一つで、割り当てられるメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。GCは、このフラグが設定されたメモリ領域をスキャンする必要がないと判断するため、GCの効率が向上します。これは、シンボルテーブルのような静的なデータや、ポインタを含まない純粋なデータ構造を割り当てる際に特に有用です。

技術的詳細

このコミットの核心は、Goランタイムがシンボルテーブル(特にファイル名や関数名などの文字列データ)を格納するために使用するメモリ領域を、ガベージコレクタがスキャンしないように変更することです。

変更前は、シンボルテーブルに関連する文字列データが runtime·gostring を介して割り当てられており、これはGCの管理下にありました。そのため、GCサイクルごとにこれらの文字列データもスキャンされ、GCのオーバーヘッドの一因となっていました。

このコミットでは、以下の主要な変更が導入されています。

  1. hugestring の導入:

    • static String hugestring;static int32 hugestring_len;src/pkg/runtime/symtab.c に追加されました。
    • hugestring は、シンボルテーブル内のすべての文字列(ファイルパスなど)を連続して格納するための単一の大きなメモリブロックとして機能します。
    • これにより、個々の文字列が小さな独立したオブジェクトとしてGCに認識されるのではなく、hugestring 全体がGCから「隠蔽」される対象となります。
  2. gostringn 関数の追加:

    • gostringn(byte *p, int32 l) という新しい関数が追加されました。
    • この関数は、指定されたバイト列 p と長さ lhugestring にコピーし、その部分文字列を表す String 型の値を返します。
    • 重要なのは、gostringnhugestring の内部に文字列を格納し、その hugestring 自体がGCの対象外となるように設計されている点です。
  3. mallocgcFlagNoPointers の利用:

    • buildfuncs 関数内で、funcfname テーブルのメモリ割り当てに 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の対象から外れます。
  4. シンボルテーブル構築の2パス処理:

    • buildfuncs 関数内で、walksymtab(dosrcline) が2回呼び出されるようになりました。
      • パス1: hugestring_len を計算するためにシンボルテーブルをウォークします。この時点では hugestring.strnil です。
      • パス2: hugestring_len で確保された hugestring に実際の文字列データを格納し、gostringn を使用して文字列スライスを作成します。
    • この2パス処理により、必要な hugestring の正確なサイズを事前に決定し、一度に連続したメモリブロックを割り当てることが可能になります。

これらの変更により、シンボルテーブルに関連するデータは、GCがスキャンする必要のないメモリ領域に配置されるようになります。これにより、GCはより少ないメモリ領域をスキャンするだけで済み、GCサイクルが短縮され、Goプログラム全体のパフォーマンスが向上します。

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

src/pkg/runtime/symtab.c における主要な変更点は以下の通りです。

  1. 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 がインクルードされました。

  2. 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 が追加されました。

  3. 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 で使用されます。

  4. 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 関数が追加されました。

  5. 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 を使って割り当てるように変更されました。

  6. 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\");\
    

    funcfname の割り当てに runtime·mallocgcFlagNoPointers が使用されるようになりました。また、dosrcline を2回呼び出すことで、hugestring のサイズを決定し、その後に実際の文字列データを格納する2パス処理が導入されました。

コアとなるコードの解説

このコミットの主要な目的は、Goランタイムのシンボルテーブルがガベージコレクタによってスキャンされないようにすることです。これを実現するために、以下のメカニズムが導入されています。

  1. hugestring による文字列の一元管理とGCからの除外:

    • Goのシンボルテーブルには、ソースファイル名や関数名などの多くの文字列が含まれています。これらの文字列が個別にGC管理下のヒープに割り当てられていると、GCはそれらすべてをスキャンする必要があります。
    • hugestring は、これらのすべての文字列を格納するための単一の大きな連続したメモリブロックとして機能します。
    • この hugestring 自体は、runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0) を使用して割り当てられます。FlagNoPointers フラグは、このメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。これにより、hugestring 全体がGCの対象から外れ、GCはシンボルテーブルの文字列データをスキャンする必要がなくなります。
  2. gostringn による効率的な文字列スライス生成:

    • gostringn 関数は、hugestring 内の特定の部分を指す String 型の値を生成します。これは、Goの文字列が内部的にポインタと長さのペアとして表現されることを利用しています。
    • gostringn は、新しいメモリを割り当てるのではなく、既存の hugestring 内のオフセットと長さを指定することで、文字列スライスを作成します。これにより、メモリコピーのオーバーヘッドが削減され、効率的な文字列管理が可能になります。
  3. 2パス処理によるメモリ割り当ての最適化:

    • buildfuncs 関数における dosrcline の2回呼び出しは、hugestring のメモリ割り当てを最適化するための重要なステップです。
    • 1回目のパス: hugestring.strnil の状態で dosrcline を実行することで、gostringn は実際の文字列コピーを行わず、必要な hugestring_len (全文字列の合計長)を計算します。
    • 2回目のパス: 1回目のパスで計算された hugestring_len を使用して、hugestring のための正確なサイズのメモリブロックが runtime·mallocgcFlagNoPointers で割り当てられます。その後、2回目の dosrcline 実行で、実際の文字列データがこの hugestring にコピーされます。
    • このアプローチにより、必要なメモリ量を正確に把握し、一度に連続したメモリブロックを割り当てることができ、メモリの断片化を防ぎ、キャッシュ効率を向上させます。
  4. func および fname テーブルのGCからの除外:

    • func (関数情報) と fname (ファイル名情報) のテーブルも、runtime·mallocgcFlagNoPointers を使用して割り当てられるようになりました。
    • これらのテーブルは、シンボルテーブルと同様に、主に静的なデータ(関数エントリポイント、ファイル名文字列へのポインタなど)を含み、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の仕組みや進化について解説されているもの。
  • Go Memory Management: 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スレッドにマッピングし、効率的に実行するためのスケジューリングを行います。
  • メモリ管理: ヒープメモリの割り当てと解放を管理します。

ガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはや参照されなくなった(到達不可能になった)メモリ領域を自動的に特定し、解放するプロセスです。これにより、開発者は手動でのメモリ管理(mallocfreeなど)から解放され、メモリリークのリスクを低減できます。

GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。

  1. マークフェーズ: GCは、プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。このプロセスでは、オブジェクト内のポインタをたどって、さらに到達可能なオブジェクトをマークしていきます。
  2. スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが、不要なメモリとして識別され、解放されます。

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)

ガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはや参照されなくなった(到達不可能になった)メモリ領域を自動的に特定し、解放するプロセスです。これにより、開発者は手動でのメモリ管理(mallocfreeなど)から解放され、メモリリークのリスクを低減できます。

GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしています。

  1. マークフェーズ: GCは、プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。このプロセスでは、オブジェクト内のポインタをたどって、さらに到達可能なオブジェクトをマークしていきます。
  2. スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが、不要なメモリとして識別され、解放されます。

GCのパフォーマンスは、マークフェーズでスキャンするメモリ領域の量に大きく依存します。スキャンするデータが多ければ多いほど、GCの実行時間は長くなり、プログラムの実行が一時停止する「ストップ・ザ・ワールド(STW)」時間が長くなる可能性があります。

シンボルテーブル (Symbol Table)

シンボルテーブルは、コンパイラやリンカによって生成されるデータ構造で、プログラム内のシンボル(変数名、関数名、ファイル名、行番号など)とそのアドレスや型情報などの関連情報をマッピングします。実行時には、デバッガがシンボルテーブルを利用して、ソースコードの行番号と実行中のマシンコードのアドレスを関連付けたり、関数名を表示したりします。

Goのバイナリには、pclntab (PC-line table) と呼ばれるテーブルが含まれており、これはプログラムカウンタ(PC)とソースコードの行番号、ファイル名を関連付けるための情報を持っています。このテーブルは、デバッグ情報やスタックトレースの生成に不可欠です。シンボルテーブルは、通常、プログラムの実行中に内容が変更されることはなく、静的なデータとして扱われます。

mallocgcFlagNoPointers

Goランタイムには、メモリを割り当てるための関数がいくつか存在します。runtime·mallocgc は、ガベージコレクタによって管理されるヒープメモリを割り当てるための関数です。この関数は、割り当てるメモリのサイズだけでなく、そのメモリ領域がポインタを含むかどうかを示すフラグを受け取ることができます。

FlagNoPointers は、runtime·mallocgc に渡されるフラグの一つで、割り当てられるメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。GCは、このフラグが設定されたメモリ領域をスキャンする必要がないと判断するため、GCの効率が向上します。これは、シンボルテーブルのような静的なデータや、ポインタを含まない純粋なデータ構造を割り当てる際に特に有用です。

技術的詳細

このコミットの核心は、Goランタイムがシンボルテーブル(特にファイル名や関数名などの文字列データ)を格納するために使用するメモリ領域を、ガベージコレクタがスキャンしないように変更することです。

変更前は、シンボルテーブルに関連する文字列データが runtime·gostring を介して割り当てられており、これはGCの管理下にありました。そのため、GCサイクルごとにこれらの文字列データもスキャンされ、GCのオーバーヘッドの一因となっていました。

このコミットでは、以下の主要な変更が導入されています。

  1. hugestring の導入:

    • static String hugestring;static int32 hugestring_len;src/pkg/runtime/symtab.c に追加されました。
    • hugestring は、シンボルテーブル内のすべての文字列(ファイルパスなど)を連続して格納するための単一の大きなメモリブロックとして機能します。
    • これにより、個々の文字列が小さな独立したオブジェクトとしてGCに認識されるのではなく、hugestring 全体がGCから「隠蔽」される対象となります。
  2. gostringn 関数の追加:

    • gostringn(byte *p, int32 l) という新しい関数が追加されました。
    • この関数は、指定されたバイト列 p と長さ lhugestring にコピーし、その部分文字列を表す String 型の値を返します。
    • 重要なのは、gostringnhugestring の内部に文字列を格納し、その hugestring 自体がGCの対象外となるように設計されている点です。
  3. mallocgcFlagNoPointers の利用:

    • buildfuncs 関数内で、funcfname テーブルのメモリ割り当てに 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の対象から外れます。
  4. シンボルテーブル構築の2パス処理:

    • buildfuncs 関数内で、walksymtab(dosrcline) が2回呼び出されるようになりました。
      • パス1: hugestring_len を計算するためにシンボルテーブルをウォークします。この時点では hugestring.strnil です。
      • パス2: hugestring_len で確保された hugestring に実際の文字列データを格納し、gostringn を使用して文字列スライスを作成します。
    • この2パス処理により、必要な hugestring の正確なサイズを事前に決定し、一度に連続したメモリブロックを割り当てることが可能になります。

これらの変更により、シンボルテーブルに関連するデータは、GCがスキャンする必要のないメモリ領域に配置されるようになります。これにより、GCはより少ないメモリ領域をスキャンするだけで済み、GCサイクルが短縮され、Goプログラム全体のパフォーマンスが向上します。

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

src/pkg/runtime/symtab.c における主要な変更点は以下の通りです。

  1. 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 がインクルードされました。

  2. 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 が追加されました。

  3. 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 で使用されます。

  4. 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 関数が追加されました。

  5. 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 を使って割り当てるように変更されました。

  6. 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\");\
    

    funcfname の割り当てに runtime·mallocgcFlagNoPointers が使用されるようになりました。また、dosrcline を2回呼び出すことで、hugestring のサイズを決定し、その後に実際の文字列データを格納する2パス処理が導入されました。

コアとなるコードの解説

このコミットの主要な目的は、Goランタイムのシンボルテーブルがガベージコレクタによってスキャンされないようにすることです。これを実現するために、以下のメカニズムが導入されています。

  1. hugestring による文字列の一元管理とGCからの除外:

    • Goのシンボルテーブルには、ソースファイル名や関数名などの多くの文字列が含まれています。これらの文字列が個別にGC管理下のヒープに割り当てられていると、GCはそれらすべてをスキャンする必要があります。
    • hugestring は、これらのすべての文字列を格納するための単一の大きな連続したメモリブロックとして機能します。
    • この hugestring 自体は、runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0) を使用して割り当てられます。FlagNoPointers フラグは、このメモリ領域がGCがスキャンすべきポインタを含まないことをGCに伝えます。これにより、hugestring 全体がGCの対象から外れ、GCはシンボルテーブルの文字列データをスキャンする必要がなくなります。
  2. gostringn による効率的な文字列スライス生成:

    • gostringn 関数は、hugestring 内の特定の部分を指す String 型の値を生成します。これは、Goの文字列が内部的にポインタと長さのペアとして表現されることを利用しています。
    • gostringn は、新しいメモリを割り当てるのではなく、既存の hugestring 内のオフセットと長さを指定することで、文字列スライスを作成します。これにより、メモリコピーのオーバーヘッドが削減され、効率的な文字列管理が可能になります。
  3. 2パス処理によるメモリ割り当ての最適化:

    • buildfuncs 関数における dosrcline の2回呼び出しは、hugestring のメモリ割り当てを最適化するための重要なステップです。
    • 1回目のパス: hugestring.strnil の状態で dosrcline を実行することで、gostringn は実際の文字列コピーを行わず、必要な hugestring_len (全文字列の合計長)を計算します。
    • 2回目のパス: 1回目のパスで計算された hugestring_len を使用して、hugestring のための正確なサイズのメモリブロックが runtime·mallocgcFlagNoPointers で割り当てられます。その後、2回目の dosrcline 実行で、実際の文字列データがこの hugestring にコピーされます。
    • このアプローチにより、必要なメモリ量を正確に把握し、一度に連続したメモリブロックを割り当てることができ、メモリの断片化を防ぎ、キャッシュ効率を向上させます。
  4. func および fname テーブルのGCからの除外:

    • func (関数情報) と fname (ファイル名情報) のテーブルも、runtime·mallocgcFlagNoPointers を使用して割り当てられるようになりました。
    • これらのテーブルは、シンボルテーブルと同様に、主に静的なデータ(関数エントリポイント、ファイル名文字列へのポインタなど)を含み、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の仕組みや進化について解説されているもの。
  • Go Memory Management: Goのメモリ管理に関する技術記事や解説。
  • Compiler Symbol Tables: コンパイラにおけるシンボルテーブルの一般的な概念に関する情報。
    • 例: Wikipediaの「シンボルテーブル」の項目など。