[インデックス 16417] ファイルの概要
このコミットは、Goランタイムにpersistentalloc()
という新しいヘルパー関数を導入し、シンボルテーブル(symtab)のメモリ割り当てを最適化することを目的としています。この変更により、ビルド時間の短縮、初期ヒープサイズの削減、そしてランタイム内の他の永続的なデータ構造の割り当て効率の向上が図られています。
コミット
commit 47e0a3d7b12bbb12a513d7d1a4ebef8632a471ae
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue May 28 10:47:35 2013 +0400
runtime: introduce helper persistentalloc() function
It is a caching wrapper around SysAlloc() that can allocate small chunks.
Use it for symtab allocations. Reduces number of symtab walks from 4 to 3
(reduces buildfuncs time from 10ms to 7.5ms on a large binary,
reduces initial heap size by 680K on the same binary).
Also can be used for type info allocation, itab allocation.
There are also several places in GC where we do the same thing,
they can be changed to use persistentalloc().
Also can be used in FixAlloc, because each instance of FixAlloc allocates
in 128K regions, which is too eager.
R=golang-dev, daniel.morsing, khr
CC=golang-dev
https://golang.org/cl/9805043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/47e0a3d7b12bbb12a513d7d1a4ebef8632a471ae
元コミット内容
このコミットは、Goランタイムにpersistentalloc()
というヘルパー関数を導入します。この関数は、SysAlloc()
(システムコールによるメモリ割り当て)のキャッシュラッパーとして機能し、特に小さなメモリチャンクの割り当てに特化しています。主な目的は、シンボルテーブル(symtab)の割り当てに使用することです。
この変更により、以下の効果が期待されます。
- シンボルテーブルのウォーク回数が4回から3回に削減されます。
- 大規模なバイナリにおける
buildfuncs
の処理時間が10msから7.5msに短縮されます。 - 同じバイナリにおける初期ヒープサイズが680KB削減されます。
さらに、persistentalloc()
は、型情報(type info)やインターフェーステーブル(itab)の割り当てにも利用できる可能性があり、ガベージコレクション(GC)の既存の同様の処理や、128KB単位で積極的に割り当てを行うFixAlloc
の改善にも応用できると述べられています。
変更の背景
Goランタイムは、プログラムの実行に必要な様々な内部データ構造を管理しています。その中には、シンボルテーブル、型情報、インターフェーステーブルなど、プログラムのライフサイクルを通じて永続的に存在し、頻繁に割り当てやアクセスが行われるデータが含まれます。
従来のメモリ割り当て方法、特にSysAlloc()
のようなシステムコールを直接利用する方法は、小さなメモリチャンクを頻繁に割り当てる場合にオーバーヘッドが大きくなる傾向があります。また、FixAlloc
のように固定サイズの大きな領域を割り当てるアロケータも、小さなオブジェクトに対してはメモリの断片化や無駄が生じる可能性があります。
このコミットの背景には、これらの永続的なデータ構造の割り当て効率を改善し、Goプログラムのビルド時間、起動時間、およびメモリ使用量を最適化するという目的があります。特に、シンボルテーブルの構築プロセスにおける複数回のウォーク(走査)がパフォーマンスボトルネックとなっていたため、これを削減することが重要な課題でした。persistentalloc()
の導入は、これらの課題に対処するための具体的な解決策として提案されました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムおよびメモリ管理に関する基本的な知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション(GC)、スケジューリング、メモリ割り当て、システムコールインターフェースなど、Go言語の並行性やメモリ安全性を実現するための多くの機能を提供します。C言語で書かれた部分が多く、GoプログラムとOSの間の橋渡しをします。
-
SysAlloc()
: Goランタイムがオペレーティングシステムから直接メモリを要求するために使用する関数です。これは通常、大きなメモリ領域を一度に確保するために使われます。mmap
やVirtualAlloc
のようなOSのシステムコールをラップしています。 -
シンボルテーブル (Symbol Table /
symtab
): コンパイルされたGoバイナリに含まれるメタデータの一部です。関数名、変数名、ファイル名、行番号などの情報が格納されており、デバッグ、プロファイリング、スタックトレースの生成などに利用されます。Goプログラムが実行時に自身のコードに関する情報を参照するために不可欠です。 -
buildfuncs
: Goランタイムの初期化プロセスの一部で、バイナリ内の関数に関するメタデータ(関数名、開始アドレス、終了アドレスなど)を構築する処理を指します。この処理は、シンボルテーブルの情報を利用して行われるため、シンボルテーブルの効率が直接影響します。 -
ヒープ (Heap): プログラムが実行時に動的にメモリを割り当てる領域です。Goでは、
make
やnew
で作成されるオブジェクトはヒープに割り当てられ、ガベージコレクタによって管理されます。初期ヒープサイズは、プログラム起動時に確保されるメモリの量を示します。 -
ガベージコレクション (Garbage Collection / GC): Goランタイムの主要な機能の一つで、プログラムが不要になったメモリを自動的に解放するプロセスです。開発者が手動でメモリを管理する必要がなくなり、メモリリークのリスクを低減します。GCはヒープ上のオブジェクトをスキャンし、到達不能なオブジェクトを特定して解放します。
-
インターフェーステーブル (
itab
): Goのインターフェースがどのように実装されているかを示す内部データ構造です。itab
は、特定の型が特定のインターフェースを実装している場合に、その型のメソッドへのポインタを格納します。これにより、Goは実行時にポリモーフィズムを実現します。itab
もまた、ランタイムによって動的に生成され、永続的に存在します。 -
FixAlloc
: Goランタイム内の別のメモリ割り当てメカニズムです。これは、固定サイズのオブジェクトを効率的に割り当てるために設計されています。通常、大きなチャンクを一度にSysAlloc
で確保し、そのチャンクを小さな固定サイズのブロックに分割して利用します。コミットメッセージでは、FixAlloc
が128KB単位で「too eager」(積極的すぎる)に割り当てる点が指摘されており、小さなオブジェクトに対してはメモリの無駄が生じる可能性が示唆されています。 -
FlagNoPointers
: GoのGCに関連するフラグの一つです。このフラグが設定されたメモリ領域は、GCがポインタをスキャンしないことを示します。これは、その領域にポインタが含まれていないか、含まれていてもGCが追跡する必要のないデータ(例えば、OSのアドレス空間への直接マッピングなど)である場合に設定されます。 -
hugestring
: このコミットで削除される、シンボルテーブル関連の文字列を格納するための古いメカニズムです。複数の文字列を一つの大きなバッファに連結して管理していました。この方式は、文字列の追加ごとにバッファの再割り当てやコピーが発生する可能性があり、効率的ではありませんでした。
技術的詳細
persistentalloc()
関数は、Goランタイムのメモリ管理における特定のニーズ、すなわち「永続的で、かつ比較的小さなサイズのオブジェクト」の効率的な割り当てに対応するために設計されました。
persistentalloc()
のメカニズム
- キャッシュ機構:
persistentalloc()
は、persistent
という静的な構造体内にpos
(現在の割り当て位置)とend
(現在のチャンクの終了位置)を保持することで、シンプルなキャッシュ機構を実装しています。 - チャンク単位の割り当て:
PersistentAllocChunk
(デフォルトで256KB)という比較的大きな単位で、一度にSysAlloc()
を呼び出してOSからメモリを確保します。これにより、頻繁なシステムコールによるオーバーヘッドを削減します。 - 小さなブロックの切り出し: 確保したチャンクの中から、要求された
size
とalign
(アライメント)に従って小さなメモリブロックを切り出して返します。 - アライメント:
align
引数により、返されるメモリブロックが指定されたバイト境界にアライメントされることを保証します。デフォルトは8バイトです。 - ロック機構:
persistent
構造体へのアクセスはruntime·lock
とruntime·unlock
によって保護されており、複数のゴルーチンからの同時アクセスに対するスレッドセーフティが確保されています。 - 最大ブロックサイズ:
PersistentAllocMaxBlock
(デフォルトで64KB)よりも大きなサイズの割り当て要求があった場合、persistentalloc()
はキャッシュ機構を使わず、直接runtime·SysAlloc()
を呼び出してメモリを確保します。これは、非常に大きな割り当てに対してはチャンクベースのキャッシュが非効率的であるためです。 - 解放操作なし:
persistentalloc()
によって割り当てられたメモリは、明示的に解放されることはありません。これは、関数名が示す通り「永続的」なデータ(プログラムの実行期間中ずっと必要とされるデータ)のために設計されているためです。ガベージコレクタもこの領域をスキャンしません。
symtab.c
における変更
hugestring
の廃止: 以前はhugestring
という単一の大きなString
バッファにシンボルテーブル関連の文字列をまとめて格納していました。この方式では、文字列を追加するたびにhugestring_len
を計算し、その後mallocgc
で実際のメモリを確保するという2パスの処理が必要でした。また、文字列の追加ごとにhugestring.len
を更新する必要がありました。persistentalloc()
への移行: 新しいgostringn
関数では、文字列の長さに応じて直接runtime·persistentalloc(l, 1)
を呼び出し、個々の文字列を永続的なメモリ領域に割り当てます。これにより、hugestring
のような中間バッファが不要になり、メモリ割り当てのプロセスが簡素化されます。buildfuncs
の最適化:buildfuncs
関数内で、関数情報(func
)やファイル名情報(fname
)の割り当てにruntime·mallocgc
の代わりにruntime·persistentalloc
が使用されるようになりました。これにより、これらのデータがGCのスキャン対象から外れ、GCのオーバーヘッドが軽減されます。また、hugestring
に関連する2パスの処理(walksymtab
の2回呼び出し)が1回に削減され、buildfuncs
の実行時間が短縮されます。
パフォーマンスへの影響
- ビルド時間の短縮: シンボルテーブルの構築プロセスにおけるメモリ割り当ての効率化と、
hugestring
関連の2パス処理の削減により、buildfuncs
の実行時間が短縮されます。 - 初期ヒープサイズの削減:
persistentalloc
がSysAlloc
をより効率的に利用し、小さなチャンクを再利用することで、プログラム起動時のメモリフットプリントが削減されます。また、GC対象外のメモリに永続データを配置することで、GCが管理するヒープのサイズも小さく保たれます。 - GCオーバーヘッドの削減:
persistentalloc
で割り当てられたメモリはGCの対象外となるため、GCがスキャンする必要のあるメモリ領域が減り、GCの実行頻度や一時停止時間が短縮される可能性があります。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下の3つのファイルに集中しています。
-
src/pkg/runtime/malloc.goc
:persistent
という静的な構造体(Lock
,pos
,end
を含む)が定義されます。PersistentAllocChunk
(256KB) とPersistentAllocMaxBlock
(64KB) の定数が定義されます。runtime·persistentalloc(uintptr size, uintptr align)
関数が新規に実装されます。この関数は、persistent
構造体を利用したキャッシュ機構と、runtime·SysAlloc
を組み合わせたメモリ割り当てロジックを含みます。
-
src/pkg/runtime/malloc.h
:runtime·persistentalloc
関数のプロトタイプ宣言が追加されます。
-
src/pkg/runtime/symtab.c
:hugestring
、hugestring_len
といったhugestring
関連の静的変数とコメントが削除されます。gostringn
関数内で、文字列の割り当てがhugestring
への追加からruntime·persistentalloc(l, 1)
への呼び出しに置き換えられます。buildfuncs
関数内で、func
とfname
の割り当てがruntime·mallocgc
からruntime·persistentalloc
に変更されます。hugestring
に関連するwalksymtab(dosrcline)
の2回目の呼び出しと、その後のhugestring
の初期化・検証ロジックが削除されます。
コアとなるコードの解説
src/pkg/runtime/malloc.goc
におけるpersistentalloc
の実装
static struct
{
Lock;
byte* pos;
byte* end;
} persistent;
enum
{
PersistentAllocChunk = 256<<10, // 256KB
PersistentAllocMaxBlock = 64<<10, // 64KB
};
// Wrapper around SysAlloc that can allocate small chunks.
// There is no associated free operation.
// Intended for things like function/type/debug-related persistent data.
// If align is 0, uses default align (currently 8).
void*
runtime·persistentalloc(uintptr size, uintptr align)
{
byte *p;
// アライメントの検証とデフォルト値の設定
if(align) {
if(align&(align-1)) // alignが2のべき乗でない場合
runtime·throw("persistentalloc: align is now a power of 2");
if(align > PageSize) // alignがページサイズより大きい場合
runtime·throw("persistentalloc: align is too large");
} else
align = 8; // デフォルトアライメント
// 要求サイズが最大ブロックサイズ以上の場合、直接SysAllocを呼び出す
if(size >= PersistentAllocMaxBlock)
return runtime·SysAlloc(size);
// persistent構造体をロック
runtime·lock(&persistent);
// 現在のposをアライメント境界に丸める
persistent.pos = (byte*)ROUND((uintptr)persistent.pos, align);
// 現在のチャンクに十分なスペースがない場合、新しいチャンクをSysAllocで確保
if(persistent.pos + size > persistent.end) {
persistent.pos = runtime·SysAlloc(PersistentAllocChunk);
if(persistent.pos == nil) {
runtime·unlock(&persistent);
runtime·throw("runtime: cannot allocate memory");
}
persistent.end = persistent.pos + PersistentAllocChunk;
}
// 現在のposからメモリブロックを切り出し、posを更新
p = persistent.pos;
persistent.pos += size;
// persistent構造体のロックを解除
runtime·unlock(&persistent);
return p;
}
この関数は、persistent
というグローバルな構造体を使って、メモリのキャッシュプールを管理します。PersistentAllocChunk
(256KB)単位でOSからメモリを確保し、その中から要求されたサイズのブロックを切り出して返します。PersistentAllocMaxBlock
(64KB)より大きな要求は直接SysAlloc
にフォールバックします。アライメントも考慮され、スレッドセーフティのためにロックが使用されます。
src/pkg/runtime/symtab.c
における変更
gostringn
関数の変更
// 変更前 (抜粋)
// static String hugestring;
// static int32 hugestring_len;
// ...
// gostringn(byte *p, int32 l) {
// ...
// if(hugestring.str == nil) {
// hugestring_len += l;
// return runtime·emptystring;
// }
// s.str = hugestring.str + hugestring.len;
// s.len = l;
// hugestring.len += s.len;
// runtime·memmove(s.str, p, l);
// return s;
// }
// 変更後 (抜粋)
static String
gostringn(byte *p, int32 l)
{
String s;
if(l == 0)
return runtime·emptystring;
s.len = l;
s.str = runtime·persistentalloc(l, 1); // persistentallocを使用
runtime·memmove(s.str, p, l);
return s;
}
変更前は、hugestring
という大きなバッファに文字列を連結していました。これは、まず文字列の合計長を計算し(hugestring.str == nil
のパス)、その後実際にメモリを割り当てて文字列をコピーするという2段階のプロセスを必要としました。
変更後は、runtime·persistentalloc(l, 1)
を直接呼び出すことで、各文字列が個別に永続的なメモリ領域に割り当てられます。これにより、hugestring
の管理が不要になり、コードが簡素化され、メモリ割り当てがより直接的になります。
buildfuncs
関数の変更
// 変更前 (抜粋)
// func = runtime·mallocgc((nfunc+1)*sizeof func[0], FlagNoPointers, 0, 1);
// fName = runtime·mallocgc(nfname*sizeof fname[0], FlagNoPointers, 0, 1);
// ...
// 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
// ...
// 変更後 (抜粋)
// Memory obtained from runtime·persistentalloc() is not scanned by GC,
// this is fine because all pointers either point into sections of the executable
// or also obtained from persistentmalloc().
func = runtime·persistentalloc((nfunc+1)*sizeof func[0], 0); // persistentallocを使用
fname = runtime·persistentalloc(nfname*sizeof fname[0], 0); // persistentallocを使用
nfunc = 0;
lastvalue = 0;
walksymtab(dofunc); // この呼び出しは変更なし
// record src file and line info for each func
files = runtime·malloc(maxfiles * sizeof(files[0]));
walksymtab(dosrcline); // 1回の呼び出しで完結
files = nil;
変更前は、func
とfname
の配列をruntime·mallocgc
で割り当てていました。また、hugestring
の処理のためにwalksymtab(dosrcline)
を2回呼び出す必要がありました。
変更後は、func
とfname
の割り当てにruntime·persistentalloc
を使用することで、これらのデータがGCの対象外となり、GCのオーバーヘッドが削減されます。さらに、hugestring
が廃止されたため、walksymtab(dosrcline)
の2回目の呼び出しが不要になり、シンボルテーブル構築のパスが1回削減され、buildfuncs
の実行時間が短縮されます。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goランタイムのソースコード: https://github.com/golang/go/tree/master/src/runtime
- Goのメモリ管理に関するブログ記事やドキュメント(一般的な情報源)
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- このコミットのGerritリンク: https://golang.org/cl/9805043
- Goのメモリ管理に関する一般的な情報源(例: Goの公式ブログ、Goの内部構造に関する技術記事など)
- Goのメモリ割り当てとGCに関する詳細な解説は、Goの公式ブログやGoのソースコードを深く掘り下げた技術記事で多く見られます。
SysAlloc
やFixAlloc
などの具体的なアロケータについては、Goランタイムのソースコード(特にsrc/runtime/malloc.go
やsrc/runtime/mheap.go
など)が最も正確な情報源となります。- シンボルテーブルや
itab
の構造については、Goのコンパイラやランタイムの内部構造に関するドキュメントや解説が参考になります。
[インデックス 16417] ファイルの概要
このコミットは、Goランタイムにpersistentalloc()
という新しいヘルパー関数を導入し、シンボルテーブル(symtab)のメモリ割り当てを最適化することを目的としています。この変更により、ビルド時間の短縮、初期ヒープサイズの削減、そしてランタイム内の他の永続的なデータ構造の割り当て効率の向上が図られています。
コミット
commit 47e0a3d7b12bbb12a513d7d1a4ebef8632a471ae
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue May 28 10:47:35 2013 +0400
runtime: introduce helper persistentalloc() function
It is a caching wrapper around SysAlloc() that can allocate small chunks.
Use it for symtab allocations. Reduces number of symtab walks from 4 to 3
(reduces buildfuncs time from 10ms to 7.5ms on a large binary,
reduces initial heap size by 680K on the same binary).
Also can be used for type info allocation, itab allocation.
There are also several places in GC where we do the same thing,
they can be changed to use persistentalloc().
Also can be used in FixAlloc, because each instance of FixAlloc allocates
in 128K regions, which is too eager.
R=golang-dev, daniel.morsing, khr
CC=golang-dev
https://golang.org/cl/9805043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/47e0a3d7b12bbb12a513d7d1a4ebef8632a471ae
元コミット内容
このコミットは、Goランタイムにpersistentalloc()
というヘルパー関数を導入します。この関数は、SysAlloc()
(システムコールによるメモリ割り当て)のキャッシュラッパーとして機能し、特に小さなメモリチャンクの割り当てに特化しています。主な目的は、シンボルテーブル(symtab)の割り当てに使用することです。
この変更により、以下の効果が期待されます。
- シンボルテーブルのウォーク回数が4回から3回に削減されます。
- 大規模なバイナリにおける
buildfuncs
の処理時間が10msから7.5msに短縮されます。 - 同じバイナリにおける初期ヒープサイズが680KB削減されます。
さらに、persistentalloc()
は、型情報(type info)やインターフェーステーブル(itab)の割り当てにも利用できる可能性があり、ガベージコレクション(GC)の既存の同様の処理や、128KB単位で積極的に割り当てを行うFixAlloc
の改善にも応用できると述べられています。
変更の背景
Goランタイムは、プログラムの実行に必要な様々な内部データ構造を管理しています。その中には、シンボルテーブル、型情報、インターフェーステーブルなど、プログラムのライフサイクルを通じて永続的に存在し、頻繁に割り当てやアクセスが行われるデータが含まれます。
従来のメモリ割り当て方法、特にSysAlloc()
のようなシステムコールを直接利用する方法は、小さなメモリチャンクを頻繁に割り当てる場合にオーバーヘッドが大きくなる傾向があります。また、FixAlloc
のように固定サイズの大きな領域を割り当てるアロケータも、小さなオブジェクトに対してはメモリの断片化や無駄が生じる可能性があります。
このコミットの背景には、これらの永続的なデータ構造の割り当て効率を改善し、Goプログラムのビルド時間、起動時間、およびメモリ使用量を最適化するという目的があります。特に、シンボルテーブルの構築プロセスにおける複数回のウォーク(走査)がパフォーマンスボトルネックとなっていたため、これを削減することが重要な課題でした。persistentalloc()
の導入は、これらの課題に対処するための具体的な解決策として提案されました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムおよびメモリ管理に関する基本的な知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション(GC)、スケジューリング、メモリ割り当て、システムコールインターフェースなど、Go言語の並行性やメモリ安全性を実現するための多くの機能を提供します。C言語で書かれた部分が多く、GoプログラムとOSの間の橋渡しをします。
-
SysAlloc()
: Goランタイムがオペレーティングシステムから直接メモリを要求するために使用する関数です。これは通常、大きなメモリ領域を一度に確保するために使われます。mmap
やVirtualAlloc
のようなOSのシステムコールをラップしています。 -
シンボルテーブル (Symbol Table /
symtab
): コンパイルされたGoバイナリに含まれるメタデータの一部です。関数名、変数名、ファイル名、行番号などの情報が格納されており、デバッグ、プロファイリング、スタックトレースの生成などに利用されます。Goプログラムが実行時に自身のコードに関する情報を参照するために不可欠です。 -
buildfuncs
: Goランタイムの初期化プロセスの一部で、バイナリ内の関数に関するメタデータ(関数名、開始アドレス、終了アドレスなど)を構築する処理を指します。この処理は、シンボルテーブルの情報を利用して行われるため、シンボルテーブルの効率が直接影響します。 -
ヒープ (Heap): プログラムが実行時に動的にメモリを割り当てる領域です。Goでは、
make
やnew
で作成されるオブジェクトはヒープに割り当てられ、ガベージコレクタによって管理されます。初期ヒープサイズは、プログラム起動時に確保されるメモリの量を示します。 -
ガベージコレクション (Garbage Collection / GC): Goランタイムの主要な機能の一つで、プログラムが不要になったメモリを自動的に解放するプロセスです。開発者が手動でメモリを管理する必要がなくなり、メモリリークのリスクを低減します。GCはヒープ上のオブジェクトをスキャンし、到達不能なオブジェクトを特定して解放します。
-
インターフェーステーブル (
itab
): Goのインターフェースがどのように実装されているかを示す内部データ構造です。itab
は、特定の型が特定のインターフェースを実装している場合に、その型のメソッドへのポインタを格納します。これにより、Goは実行時にポリモーフィズムを実現します。itab
もまた、ランタイムによって動的に生成され、永続的に存在します。 -
FixAlloc
: Goランタイム内の別のメモリ割り当てメカニズムです。これは、固定サイズのオブジェクトを効率的に割り当てるために設計されています。通常、大きなチャンクを一度にSysAlloc
で確保し、そのチャンクを小さな固定サイズのブロックに分割して利用します。コミットメッセージでは、FixAlloc
が128KB単位で「too eager」(積極的すぎる)に割り当てる点が指摘されており、小さなオブジェクトに対してはメモリの無駄が生じる可能性が示唆されています。 -
FlagNoPointers
: GoのGCに関連するフラグの一つです。このフラグが設定されたメモリ領域は、GCがポインタをスキャンしないことを示します。これは、その領域にポインタが含まれていないか、含まれていてもGCが追跡する必要のないデータ(例えば、OSのアドレス空間への直接マッピングなど)である場合に設定されます。 -
hugestring
: このコミットで削除される、シンボルテーブル関連の文字列を格納するための古いメカニズムです。複数の文字列を一つの大きなバッファに連結して管理していました。この方式は、文字列の追加ごとにバッファの再割り当てやコピーが発生する可能性があり、効率的ではありませんでした。
技術的詳細
persistentalloc()
関数は、Goランタイムのメモリ管理における特定のニーズ、すなわち「永続的で、かつ比較的小さなサイズのオブジェクト」の効率的な割り当てに対応するために設計されました。
persistentalloc()
のメカニズム
- キャッシュ機構:
persistentalloc()
は、persistent
という静的な構造体内にpos
(現在の割り当て位置)とend
(現在のチャンクの終了位置)を保持することで、シンプルなキャッシュ機構を実装しています。 - チャンク単位の割り当て:
PersistentAllocChunk
(デフォルトで256KB)という比較的大きな単位で、一度にSysAlloc()
を呼び出してOSからメモリを確保します。これにより、頻繁なシステムコールによるオーバーヘッドを削減します。 - 小さなブロックの切り出し: 確保したチャンクの中から、要求された
size
とalign
(アライメント)に従って小さなメモリブロックを切り出して返します。 - アライメント:
align
引数により、返されるメモリブロックが指定されたバイト境界にアライメントされることを保証します。デフォルトは8バイトです。 - ロック機構:
persistent
構造体へのアクセスはruntime·lock
とruntime·unlock
によって保護されており、複数のゴルーチンからの同時アクセスに対するスレッドセーフティが確保されています。 - 最大ブロックサイズ:
PersistentAllocMaxBlock
(デフォルトで64KB)よりも大きなサイズの割り当て要求があった場合、persistentalloc()
はキャッシュ機構を使わず、直接runtime·SysAlloc()
を呼び出してメモリを確保します。これは、非常に大きな割り当てに対してはチャンクベースのキャッシュが非効率的であるためです。 - 解放操作なし:
persistentalloc()
によって割り当てられたメモリは、明示的に解放されることはありません。これは、関数名が示す通り「永続的」なデータ(プログラムの実行期間中ずっと必要とされるデータ)のために設計されているためです。ガベージコレクタもこの領域をスキャンしません。
symtab.c
における変更
hugestring
の廃止: 以前はhugestring
という単一の大きなString
バッファにシンボルテーブル関連の文字列をまとめて格納していました。この方式では、文字列を追加するたびにhugestring_len
を計算し、その後mallocgc
で実際のメモリを確保するという2パスの処理が必要でした。また、文字列の追加ごとにhugestring.len
を更新する必要がありました。persistentalloc()
への移行: 新しいgostringn
関数では、文字列の長さに応じて直接runtime·persistentalloc(l, 1)
を呼び出し、個々の文字列を永続的なメモリ領域に割り当てます。これにより、hugestring
のような中間バッファが不要になり、メモリ割り当てのプロセスが簡素化されます。buildfuncs
の最適化:buildfuncs
関数内で、関数情報(func
)やファイル名情報(fname
)の割り当てにruntime·mallocgc
の代わりにruntime·persistentalloc
が使用されるようになりました。これにより、これらのデータがGCのスキャン対象から外れ、GCのオーバーヘッドが軽減されます。また、hugestring
に関連する2パスの処理(walksymtab
の2回呼び出し)が1回に削減され、buildfuncs
の実行時間が短縮されます。
パフォーマンスへの影響
- ビルド時間の短縮: シンボルテーブルの構築プロセスにおけるメモリ割り当ての効率化と、
hugestring
関連の2パス処理の削減により、buildfuncs
の実行時間が短縮されます。 - 初期ヒープサイズの削減:
persistentalloc
がSysAlloc
をより効率的に利用し、小さなチャンクを再利用することで、プログラム起動時のメモリフットプリントが削減されます。また、GC対象外のメモリに永続データを配置することで、GCが管理するヒープのサイズも小さく保たれます。 - GCオーバーヘッドの削減:
persistentalloc
で割り当てられたメモリはGCの対象外となるため、GCがスキャンする必要のあるメモリ領域が減り、GCの実行頻度や一時停止時間が短縮される可能性があります。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下の3つのファイルに集中しています。
-
src/pkg/runtime/malloc.goc
:persistent
という静的な構造体(Lock
,pos
,end
を含む)が定義されます。PersistentAllocChunk
(256KB) とPersistentAllocMaxBlock
(64KB) の定数が定義されます。runtime·persistentalloc(uintptr size, uintptr align)
関数が新規に実装されます。この関数は、persistent
構造体を利用したキャッシュ機構と、runtime·SysAlloc
を組み合わせたメモリ割り当てロジックを含みます。
-
src/pkg/runtime/malloc.h
:runtime·persistentalloc
関数のプロトタイプ宣言が追加されます。
-
src/pkg/runtime/symtab.c
:hugestring
、hugestring_len
といったhugestring
関連の静的変数とコメントが削除されます。gostringn
関数内で、文字列の割り当てがhugestring
への追加からruntime·persistentalloc(l, 1)
への呼び出しに置き換えられます。buildfuncs
関数内で、func
とfname
の割り当てがruntime·mallocgc
からruntime·persistentalloc
に変更されます。hugestring
に関連するwalksymtab(dosrcline)
の2回目の呼び出しと、その後のhugestring
の初期化・検証ロジックが削除されます。
コアとなるコードの解説
src/pkg/runtime/malloc.goc
におけるpersistentalloc
の実装
static struct
{
Lock;
byte* pos;
byte* end;
} persistent;
enum
{
PersistentAllocChunk = 256<<10, // 256KB
PersistentAllocMaxBlock = 64<<10, // 64KB
};
// Wrapper around SysAlloc that can allocate small chunks.
// There is no associated free operation.
// Intended for things like function/type/debug-related persistent data.
// If align is 0, uses default align (currently 8).
void*
runtime·persistentalloc(uintptr size, uintptr align)
{
byte *p;
// アライメントの検証とデフォルト値の設定
if(align) {
if(align&(align-1)) // alignが2のべき乗でない場合
runtime·throw("persistentalloc: align is now a power of 2");
if(align > PageSize) // alignがページサイズより大きい場合
runtime·throw("persistentalloc: align is too large");
} else
align = 8; // デフォルトアライメント
// 要求サイズが最大ブロックサイズ以上の場合、直接SysAllocを呼び出す
if(size >= PersistentAllocMaxBlock)
return runtime·SysAlloc(size);
// persistent構造体をロック
runtime·lock(&persistent);
// 現在のposをアライメント境界に丸める
persistent.pos = (byte*)ROUND((uintptr)persistent.pos, align);
// 現在のチャンクに十分なスペースがない場合、新しいチャンクをSysAllocで確保
if(persistent.pos + size > persistent.end) {
persistent.pos = runtime·SysAlloc(PersistentAllocChunk);
if(persistent.pos == nil) {
runtime·unlock(&persistent);
runtime·throw("runtime: cannot allocate memory");
}
persistent.end = persistent.pos + PersistentAllocChunk;
}
// 現在のposからメモリブロックを切り出し、posを更新
p = persistent.pos;
persistent.pos += size;
// persistent構造体のロックを解除
runtime·unlock(&persistent);
return p;
}
この関数は、persistent
というグローバルな構造体を使って、メモリのキャッシュプールを管理します。PersistentAllocChunk
(256KB)単位でOSからメモリを確保し、その中から要求されたサイズのブロックを切り出して返します。PersistentAllocMaxBlock
(64KB)より大きな要求は直接SysAlloc
にフォールバックします。アライメントも考慮され、スレッドセーフティのためにロックが使用されます。
src/pkg/runtime/symtab.c
における変更
gostringn
関数の変更
// 変更前 (抜粋)
// static String hugestring;
// static int32 hugestring_len;
// ...
// gostringn(byte *p, int32 l) {
// ...
// if(hugestring.str == nil) {
// hugestring_len += l;
// return runtime·emptystring;
// }
// s.str = hugestring.str + hugestring.len;
// s.len = l;
// hugestring.len += s.len;
// runtime·memmove(s.str, p, l);
// return s;
// }
// 変更後 (抜粋)
static String
gostringn(byte *p, int32 l)
{
String s;
if(l == 0)
return runtime·emptystring;
s.len = l;
s.str = runtime·persistentalloc(l, 1); // persistentallocを使用
runtime·memmove(s.str, p, l);
return s;
}
変更前は、hugestring
という大きなバッファに文字列を連結していました。これは、まず文字列の合計長を計算し(hugestring.str == nil
のパス)、その後実際にメモリを割り当てて文字列をコピーするという2段階のプロセスを必要としました。
変更後は、runtime·persistentalloc(l, 1)
を直接呼び出すことで、各文字列が個別に永続的なメモリ領域に割り当てられます。これにより、hugestring
の管理が不要になり、コードが簡素化され、メモリ割り当てがより直接的になります。
buildfuncs
関数の変更
// 変更前 (抜粋)
// func = runtime·mallocgc((nfunc+1)*sizeof func[0], FlagNoPointers, 0, 1);
// fName = runtime·mallocgc(nfname*sizeof fname[0], FlagNoPointers, 0, 1);
// ...
// 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
// ...
// 変更後 (抜粋)
// Memory obtained from runtime·persistentalloc() is not scanned by GC,
// this is fine because all pointers either point into sections of the executable
// or also obtained from persistentmalloc().
func = runtime·persistentalloc((nfunc+1)*sizeof func[0], 0); // persistentallocを使用
fname = runtime·persistentalloc(nfname*sizeof fname[0], 0); // persistentallocを使用
nfunc = 0;
lastvalue = 0;
walksymtab(dofunc); // この呼び出しは変更なし
// record src file and line info for each func
files = runtime·malloc(maxfiles * sizeof(files[0]));
walksymtab(dosrcline); // 1回の呼び出しで完結
files = nil;
変更前は、func
とfname
の配列をruntime·mallocgc
で割り当てていました。また、hugestring
の処理のためにwalksymtab(dosrcline)
を2回呼び出す必要がありました。
変更後は、func
とfname
の割り当てにruntime·persistentalloc
を使用することで、これらのデータがGCの対象外となり、GCのオーバーヘッドが削減されます。さらに、hugestring
が廃止されたため、walksymtab(dosrcline)
の2回目の呼び出しが不要になり、シンボルテーブル構築のパスが1回削減され、buildfuncs
の実行時間が短縮されます。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goランタイムのソースコード: https://github.com/golang/go/tree/master/src/runtime
- Goのメモリ管理に関するブログ記事やドキュメント(一般的な情報源)
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- このコミットのGerritリンク: https://golang.org/cl/9805043
- Goのメモリ管理に関する一般的な情報源(例: Goの公式ブログ、Goの内部構造に関する技術記事など)
- Goのメモリ割り当てとGCに関する詳細な解説は、Goの公式ブログやGoのソースコードを深く掘り下げた技術記事で多く見られます。
SysAlloc
やFixAlloc
などの具体的なアロケータについては、Goランタイムのソースコード(特にsrc/runtime/malloc.go
やsrc/runtime/mheap.go
など)が最も正確な情報源となります。- シンボルテーブルや
itab
の構造については、Goのコンパイラやランタイムの内部構造に関するドキュメントや解説が参考になります。