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

[インデックス 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ランタイムがオペレーティングシステムから直接メモリを要求するために使用する関数です。これは通常、大きなメモリ領域を一度に確保するために使われます。mmapVirtualAllocのようなOSのシステムコールをラップしています。

  • シンボルテーブル (Symbol Table / symtab): コンパイルされたGoバイナリに含まれるメタデータの一部です。関数名、変数名、ファイル名、行番号などの情報が格納されており、デバッグ、プロファイリング、スタックトレースの生成などに利用されます。Goプログラムが実行時に自身のコードに関する情報を参照するために不可欠です。

  • buildfuncs: Goランタイムの初期化プロセスの一部で、バイナリ内の関数に関するメタデータ(関数名、開始アドレス、終了アドレスなど)を構築する処理を指します。この処理は、シンボルテーブルの情報を利用して行われるため、シンボルテーブルの効率が直接影響します。

  • ヒープ (Heap): プログラムが実行時に動的にメモリを割り当てる領域です。Goでは、makenewで作成されるオブジェクトはヒープに割り当てられ、ガベージコレクタによって管理されます。初期ヒープサイズは、プログラム起動時に確保されるメモリの量を示します。

  • ガベージコレクション (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()のメカニズム

  1. キャッシュ機構: persistentalloc()は、persistentという静的な構造体内にpos(現在の割り当て位置)とend(現在のチャンクの終了位置)を保持することで、シンプルなキャッシュ機構を実装しています。
  2. チャンク単位の割り当て: PersistentAllocChunk(デフォルトで256KB)という比較的大きな単位で、一度にSysAlloc()を呼び出してOSからメモリを確保します。これにより、頻繁なシステムコールによるオーバーヘッドを削減します。
  3. 小さなブロックの切り出し: 確保したチャンクの中から、要求されたsizealign(アライメント)に従って小さなメモリブロックを切り出して返します。
  4. アライメント: align引数により、返されるメモリブロックが指定されたバイト境界にアライメントされることを保証します。デフォルトは8バイトです。
  5. ロック機構: persistent構造体へのアクセスはruntime·lockruntime·unlockによって保護されており、複数のゴルーチンからの同時アクセスに対するスレッドセーフティが確保されています。
  6. 最大ブロックサイズ: PersistentAllocMaxBlock(デフォルトで64KB)よりも大きなサイズの割り当て要求があった場合、persistentalloc()はキャッシュ機構を使わず、直接runtime·SysAlloc()を呼び出してメモリを確保します。これは、非常に大きな割り当てに対してはチャンクベースのキャッシュが非効率的であるためです。
  7. 解放操作なし: 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の実行時間が短縮されます。
  • 初期ヒープサイズの削減: persistentallocSysAllocをより効率的に利用し、小さなチャンクを再利用することで、プログラム起動時のメモリフットプリントが削減されます。また、GC対象外のメモリに永続データを配置することで、GCが管理するヒープのサイズも小さく保たれます。
  • GCオーバーヘッドの削減: persistentallocで割り当てられたメモリはGCの対象外となるため、GCがスキャンする必要のあるメモリ領域が減り、GCの実行頻度や一時停止時間が短縮される可能性があります。

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

このコミットのコアとなる変更は、主に以下の3つのファイルに集中しています。

  1. src/pkg/runtime/malloc.goc:

    • persistentという静的な構造体(Lock, pos, endを含む)が定義されます。
    • PersistentAllocChunk (256KB) と PersistentAllocMaxBlock (64KB) の定数が定義されます。
    • runtime·persistentalloc(uintptr size, uintptr align)関数が新規に実装されます。この関数は、persistent構造体を利用したキャッシュ機構と、runtime·SysAllocを組み合わせたメモリ割り当てロジックを含みます。
  2. src/pkg/runtime/malloc.h:

    • runtime·persistentalloc関数のプロトタイプ宣言が追加されます。
  3. src/pkg/runtime/symtab.c:

    • hugestringhugestring_lenといったhugestring関連の静的変数とコメントが削除されます。
    • gostringn関数内で、文字列の割り当てがhugestringへの追加からruntime·persistentalloc(l, 1)への呼び出しに置き換えられます。
    • buildfuncs関数内で、funcfnameの割り当てが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;

変更前は、funcfnameの配列をruntime·mallocgcで割り当てていました。また、hugestringの処理のためにwalksymtab(dosrcline)を2回呼び出す必要がありました。 変更後は、funcfnameの割り当てにruntime·persistentallocを使用することで、これらのデータがGCの対象外となり、GCのオーバーヘッドが削減されます。さらに、hugestringが廃止されたため、walksymtab(dosrcline)の2回目の呼び出しが不要になり、シンボルテーブル構築のパスが1回削減され、buildfuncsの実行時間が短縮されます。

関連リンク

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
  • Goのメモリ管理に関する一般的な情報源(例: Goの公式ブログ、Goの内部構造に関する技術記事など)
    • Goのメモリ割り当てとGCに関する詳細な解説は、Goの公式ブログやGoのソースコードを深く掘り下げた技術記事で多く見られます。
    • SysAllocFixAllocなどの具体的なアロケータについては、Goランタイムのソースコード(特にsrc/runtime/malloc.gosrc/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ランタイムがオペレーティングシステムから直接メモリを要求するために使用する関数です。これは通常、大きなメモリ領域を一度に確保するために使われます。mmapVirtualAllocのようなOSのシステムコールをラップしています。

  • シンボルテーブル (Symbol Table / symtab): コンパイルされたGoバイナリに含まれるメタデータの一部です。関数名、変数名、ファイル名、行番号などの情報が格納されており、デバッグ、プロファイリング、スタックトレースの生成などに利用されます。Goプログラムが実行時に自身のコードに関する情報を参照するために不可欠です。

  • buildfuncs: Goランタイムの初期化プロセスの一部で、バイナリ内の関数に関するメタデータ(関数名、開始アドレス、終了アドレスなど)を構築する処理を指します。この処理は、シンボルテーブルの情報を利用して行われるため、シンボルテーブルの効率が直接影響します。

  • ヒープ (Heap): プログラムが実行時に動的にメモリを割り当てる領域です。Goでは、makenewで作成されるオブジェクトはヒープに割り当てられ、ガベージコレクタによって管理されます。初期ヒープサイズは、プログラム起動時に確保されるメモリの量を示します。

  • ガベージコレクション (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()のメカニズム

  1. キャッシュ機構: persistentalloc()は、persistentという静的な構造体内にpos(現在の割り当て位置)とend(現在のチャンクの終了位置)を保持することで、シンプルなキャッシュ機構を実装しています。
  2. チャンク単位の割り当て: PersistentAllocChunk(デフォルトで256KB)という比較的大きな単位で、一度にSysAlloc()を呼び出してOSからメモリを確保します。これにより、頻繁なシステムコールによるオーバーヘッドを削減します。
  3. 小さなブロックの切り出し: 確保したチャンクの中から、要求されたsizealign(アライメント)に従って小さなメモリブロックを切り出して返します。
  4. アライメント: align引数により、返されるメモリブロックが指定されたバイト境界にアライメントされることを保証します。デフォルトは8バイトです。
  5. ロック機構: persistent構造体へのアクセスはruntime·lockruntime·unlockによって保護されており、複数のゴルーチンからの同時アクセスに対するスレッドセーフティが確保されています。
  6. 最大ブロックサイズ: PersistentAllocMaxBlock(デフォルトで64KB)よりも大きなサイズの割り当て要求があった場合、persistentalloc()はキャッシュ機構を使わず、直接runtime·SysAlloc()を呼び出してメモリを確保します。これは、非常に大きな割り当てに対してはチャンクベースのキャッシュが非効率的であるためです。
  7. 解放操作なし: 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の実行時間が短縮されます。
  • 初期ヒープサイズの削減: persistentallocSysAllocをより効率的に利用し、小さなチャンクを再利用することで、プログラム起動時のメモリフットプリントが削減されます。また、GC対象外のメモリに永続データを配置することで、GCが管理するヒープのサイズも小さく保たれます。
  • GCオーバーヘッドの削減: persistentallocで割り当てられたメモリはGCの対象外となるため、GCがスキャンする必要のあるメモリ領域が減り、GCの実行頻度や一時停止時間が短縮される可能性があります。

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

このコミットのコアとなる変更は、主に以下の3つのファイルに集中しています。

  1. src/pkg/runtime/malloc.goc:

    • persistentという静的な構造体(Lock, pos, endを含む)が定義されます。
    • PersistentAllocChunk (256KB) と PersistentAllocMaxBlock (64KB) の定数が定義されます。
    • runtime·persistentalloc(uintptr size, uintptr align)関数が新規に実装されます。この関数は、persistent構造体を利用したキャッシュ機構と、runtime·SysAllocを組み合わせたメモリ割り当てロジックを含みます。
  2. src/pkg/runtime/malloc.h:

    • runtime·persistentalloc関数のプロトタイプ宣言が追加されます。
  3. src/pkg/runtime/symtab.c:

    • hugestringhugestring_lenといったhugestring関連の静的変数とコメントが削除されます。
    • gostringn関数内で、文字列の割り当てがhugestringへの追加からruntime·persistentalloc(l, 1)への呼び出しに置き換えられます。
    • buildfuncs関数内で、funcfnameの割り当てが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;

変更前は、funcfnameの配列をruntime·mallocgcで割り当てていました。また、hugestringの処理のためにwalksymtab(dosrcline)を2回呼び出す必要がありました。 変更後は、funcfnameの割り当てにruntime·persistentallocを使用することで、これらのデータがGCの対象外となり、GCのオーバーヘッドが削減されます。さらに、hugestringが廃止されたため、walksymtab(dosrcline)の2回目の呼び出しが不要になり、シンボルテーブル構築のパスが1回削減され、buildfuncsの実行時間が短縮されます。

関連リンク

参考にした情報源リンク

  • Goのコミット履歴: https://github.com/golang/go/commits/master
  • Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
  • Goのメモリ管理に関する一般的な情報源(例: Goの公式ブログ、Goの内部構造に関する技術記事など)
    • Goのメモリ割り当てとGCに関する詳細な解説は、Goの公式ブログやGoのソースコードを深く掘り下げた技術記事で多く見られます。
    • SysAllocFixAllocなどの具体的なアロケータについては、Goランタイムのソースコード(特にsrc/runtime/malloc.gosrc/runtime/mheap.goなど)が最も正確な情報源となります。
    • シンボルテーブルやitabの構造については、Goのコンパイラやランタイムの内部構造に関するドキュメントや解説が参考になります。