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

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

このコミットは、Goランタイムにpersistentalloc()というヘルパー関数を導入するものです。この関数は、SysAlloc()のキャッシュラッパーとして機能し、特に小さなチャンクのメモリ割り当てを効率化します。シンボルテーブル(symtab)の割り当てに利用することで、ビルド時間と初期ヒープサイズを削減し、将来的には型情報やitab(インターフェーステーブル)の割り当て、さらにはGC(ガベージコレクション)やFixAllocの領域でも利用される可能性が示唆されています。

コミット

commit 86da989ee53f85044c04a418e5ec30e111d169b0
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri May 31 10:42:30 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.
    Reincarnation of committed and rolled back https://golang.org/cl/9805043
    The latent bugs that it revealed are fixed:
    https://golang.org/cl/9837049
    https://golang.org/cl/9778048
    
    R=golang-dev, khr
    CC=golang-dev
    https://golang.org/cl/9778049

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

https://github.com/golang/go/commit/86da989ee53f85044c04a418e5ec30e111d169b0

元コミット内容

このコミットは、以前にコミットされ、その後ロールバックされた変更(CL 9805043)の再実装です。以前の試みで明らかになった潜在的なバグ(CL 9837049とCL 9778048で修正済み)が解決された上で、再度導入されています。

変更の背景

Goランタイムでは、シンボルテーブル、型情報、itabなど、プログラムの実行中に永続的に必要となるが、頻繁には解放されないデータ構造が多数存在します。これらのデータは、通常、SysAllocのようなシステムコールを通じて直接メモリを割り当てていました。しかし、SysAllocはOSに直接メモリを要求するため、小さなメモリチャンクを頻繁に割り当てると、システムコールのオーバーヘッドが大きくなり、パフォーマンスに影響を与える可能性がありました。

特に、シンボルテーブルの構築プロセスでは、文字列の割り当てのためにhugestringという大きなバッファを事前に確保し、そこに文字列をコピーするという多段階の処理が行われていました。このプロセスは、シンボルテーブルを複数回走査する必要があり、ビルド時間とメモリ使用量の両面で非効率でした。

また、FixAllocのような既存のメモリ割り当てメカニズムも、各インスタンスが128KBといった比較的大きな領域を一度に割り当てるため、小さなオブジェクトの割り当てには過剰なメモリ消費を伴うことがありました。

これらの問題を解決し、ランタイムのメモリ割り当て効率とパフォーマンスを向上させるために、よりきめ細かく、かつオーバーヘッドの少ない永続的なメモリ割り当てメカニズムが必要とされていました。

前提知識の解説

Goランタイムのメモリ管理

Goランタイムは、独自のメモリ管理システムを持っています。これは、OSからのメモリ割り当て(SysAlloc)を抽象化し、ガベージコレクタ(GC)と連携して、アプリケーションが必要とするメモリを効率的に提供します。

  • SysAlloc: GoランタイムがOSから直接メモリを要求する際に使用する低レベルの関数です。通常、ページ単位(4KBなど)でメモリを割り当てます。
  • FixAlloc: 固定サイズのオブジェクトを効率的に割り当てるためのアロケータです。例えば、M Span(メモリブロックの管理単位)のようなランタイム内部のデータ構造の割り当てに使用されます。各FixAllocインスタンスは、一度に比較的大きなメモリ領域(例: 128KB)を確保し、その中から小さな固定サイズのオブジェクトを割り当てます。
  • mallocgc: Goプログラムがヒープメモリを割り当てる際に使用する主要な関数です。ガベージコレクタによって管理されるメモリ領域から割り当てが行われます。
  • シンボルテーブル(Symtab): Goのバイナリには、関数名、変数名、ファイル名、行番号などのデバッグ情報やリフレクション情報が含まれています。これらはシンボルテーブルとして管理され、プログラムの実行時やデバッグ時に利用されます。
  • itab(Interface Table): Goのインターフェース型が、具体的な型とメソッドの実装を関連付けるために使用する内部的なデータ構造です。インターフェースのメソッド呼び出しの際に、動的なディスパッチを可能にします。
  • 型情報: Goの型システムに関する情報(構造体のフィールド、メソッド、配列の要素型など)もランタイムによって管理され、リフレクションやGCの際に利用されます。

メモリ割り当ての課題

  • システムコールオーバーヘッド: SysAllocのようなシステムコールは、ユーザーモードからカーネルモードへの切り替えを伴うため、比較的コストが高い操作です。小さなメモリチャンクを頻繁に割り当てると、このオーバーヘッドが蓄積され、パフォーマンスのボトルネックとなる可能性があります。
  • メモリの断片化: 小さなメモリチャンクを不規則に割り当て、解放を繰り返すと、メモリが断片化し、大きな連続したメモリ領域が利用できなくなることがあります。
  • GCの負担: ガベージコレクタが管理するヒープ領域に割り当てられたオブジェクトは、GCの対象となります。永続的に使用されるオブジェクトであっても、GCのサイクル中にスキャンされるため、GCのオーバーヘッドを増大させる可能性があります。

技術的詳細

persistentalloc()関数は、これらの課題に対処するために導入されました。その主な特徴とメカニズムは以下の通りです。

  1. キャッシュ機構: persistentalloc()は、SysAlloc()を直接呼び出すのではなく、内部にpersistentという構造体で管理されるキャッシュ領域を持っています。このキャッシュは、PersistentAllocChunk(256KB)単位でSysAlloc()からメモリを事前に確保します。
  2. 小さなチャンクの効率的な割り当て: ユーザーがpersistentalloc()に小さなメモリチャンク(PersistentAllocMaxBlock、64KB未満)を要求した場合、このキャッシュ領域から直接割り当てが行われます。これにより、頻繁なSysAlloc()呼び出しを回避し、システムコールオーバーヘッドを削減します。
  3. アライメントのサポート: 割り当てられたメモリは、指定されたアライメント(デフォルトは8バイト)に揃えられます。これは、特定のデータ構造が特定のメモリ境界に配置されることを要求する場合に重要です。
  4. 解放操作なし: persistentalloc()によって割り当てられたメモリは、明示的に解放されることはありません。これは、シンボルテーブルや型情報のように、プログラムのライフサイクルを通じて永続的に存在する必要があるデータに適しています。GCの対象外となるため、GCの負担も軽減されます。
  5. スレッドセーフティ: persistent構造体にはLockが組み込まれており、複数のゴルーチンから同時にpersistentalloc()が呼び出されても、メモリ割り当て処理が安全に行われるように保護されています。
  6. シンボルテーブル割り当ての改善:
    • 以前のsymtab.cでは、hugestringという大きなバッファを使い、シンボル文字列をそこにコピーしていました。このプロセスは、walksymtabを複数回(2回)実行してhugestring_lenを計算し、その後実際に文字列をコピーするという非効率なものでした。
    • persistentalloc()の導入により、gostringn関数がruntime·persistentallocを使用して直接文字列を割り当てるようになりました。これにより、hugestringが不要になり、walksymtabの実行回数が削減され、シンボルテーブル構築の効率が向上しました。コミットメッセージによると、これによりbuildfuncsの時間が10msから7.5msに短縮され、初期ヒープサイズが680KB削減されたとあります。
  7. 将来的な応用: コミットメッセージでは、型情報、itab、GCの内部処理、およびFixAllocの代替としてもpersistentalloc()が利用できる可能性が示唆されています。特にFixAllocが128KB単位でメモリを確保するのに対し、persistentalloc()はより小さなチャンクを効率的に扱えるため、メモリ使用量の最適化に貢献できます。

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

src/pkg/runtime/malloc.goc

  • persistent構造体とPersistentAllocChunkPersistentAllocMaxBlock定数が追加されました。
  • runtime·persistentalloc関数が追加されました。この関数は、persistent構造体内のキャッシュを利用してメモリを割り当てます。
// 新しく追加されたpersistentalloc関数の定義
static struct
{
	Lock;
	byte*	pos;
	byte*	end;
} persistent;

enum
{
	PersistentAllocChunk	= 256<<10, // 256KB
	PersistentAllocMaxBlock	= 64<<10,  // 64KB
};

void*
runtime·persistentalloc(uintptr size, uintptr align)
{
	byte *p;

	if(align) {
		if(align&(align-1))
			runtime·throw("persistentalloc: align is now a power of 2");
		if(align > PageSize)
			runtime·throw("persistentalloc: align is too large");
	} else
		align = 8; // デフォルトのアライメント

	if(size >= PersistentAllocMaxBlock) // 64KB以上の大きな割り当ては直接SysAlloc
		return runtime·SysAlloc(size);

	runtime·lock(&persistent); // 排他ロック
	persistent.pos = (byte*)ROUND((uintptr)persistent.pos, align); // アライメント調整
	if(persistent.pos + size > persistent.end) { // キャッシュが足りない場合
		persistent.pos = runtime·SysAlloc(PersistentAllocChunk); // 新しいチャンクをSysAlloc
		if(persistent.pos == nil) {
			runtime·unlock(&persistent);
			runtime·throw("runtime: cannot allocate memory");
		}
		persistent.end = persistent.pos + PersistentAllocChunk;
	}
	p = persistent.pos;
	persistent.pos += size;
	runtime·unlock(&persistent);
	return p; 
}

src/pkg/runtime/malloc.h

  • runtime·persistentalloc関数のプロトタイプ宣言が追加されました。
void*	runtime·persistentalloc(uintptr size, uintptr align);

src/pkg/runtime/symtab.c

  • hugestring関連の変数(hugestring, hugestring_len)とそれらを使用するロジックが削除されました。
  • gostringn関数内で、文字列の割り当てにruntime·persistentallocが使用されるようになりました。
  • runtime·symtabinit関数内で、funcfnameの割り当てにruntime·persistentallocが使用されるようになりました。
  • walksymtab(dosrcline)の呼び出しが1回に削減されました(以前は2回)。
// 削除されたhugestring関連の変数
// static String hugestring;
// static int32 hugestring_len;

// gostringn関数の変更
static String
gostringn(byte *p, int32 l)
{
	String s;

	if(l == 0)
		return runtime·emptystring;
	// 以前のhugestringへの依存が削除され、persistentallocを使用
	s.str = runtime·persistentalloc(l, 1); // persistentallocで文字列を割り当て
	s.len = l;
	runtime·memmove(s.str, p, l); // 元の文字列をコピー
	return s;
}

// runtime·symtabinit関数の変更
void runtime·symtabinit(void)
{
    // ...
    // funcとfnameの割り当てにpersistentallocを使用
    func = runtime·persistentalloc((nfunc+1)*sizeof func[0], 0);
    // ...
    fname = runtime·persistentalloc(nfname*sizeof fname[0], 0);
    // ...

    // walksymtab(dosrcline)の呼び出しが1回に削減
    // 以前のhugestring_len計算とhugestringへのコピーの2パスが不要に
    walksymtab(dosrcline);
    // ...
}

コアとなるコードの解説

このコミットの核心は、runtime/malloc.gocに追加されたruntime·persistentalloc関数です。

この関数は、GoランタイムがOSから直接メモリを要求するruntime·SysAllocの呼び出し回数を減らすためのキャッシュ層として機能します。

  1. キャッシュの管理: persistentというグローバル構造体が、現在のキャッシュ領域の開始位置(pos)と終了位置(end)を管理します。
  2. チャンク単位の確保: persistentallocが呼び出され、現在のキャッシュ領域に十分なスペースがない場合、PersistentAllocChunk(256KB)単位でruntime·SysAllocを呼び出し、新しい大きなメモリブロックをOSから取得します。
  3. 小さな割り当ての効率化: ユーザーが要求するサイズがPersistentAllocMaxBlock(64KB)未満の場合、persistentallocは既存のキャッシュ領域から直接メモリを切り出して返します。これにより、小さな割り当てごとにSysAllocを呼び出すオーバーヘッドがなくなります。
  4. アライメント: 割り当てられたメモリは、指定されたアライメント(デフォルトは8バイト)に揃えられます。これは、CPUが特定のデータ型を効率的にアクセスするために必要な場合があります。
  5. 永続性: この関数で割り当てられたメモリは、明示的に解放されることはありません。これは、プログラムの実行中に常に存在し続けるデータ(シンボルテーブル、型情報など)に最適です。GCの対象外となるため、GCのパフォーマンスにも寄与します。

symtab.cにおける変更は、このpersistentallocの具体的な適用例を示しています。以前は、シンボル文字列を格納するためにhugestringという一時的な大きなバッファを使い、文字列をそこにコピーするという非効率な方法が取られていました。persistentallocの導入により、gostringn関数は文字列が必要になった時点でpersistentallocを使って直接メモリを割り当て、そこに文字列をコピーできるようになりました。これにより、hugestringが不要になり、シンボルテーブルの構築プロセスが簡素化され、メモリ使用量とビルド時間の両方が改善されました。

また、funcfnameというシンボルテーブル関連のデータ構造の割り当てもruntime·mallocgcからruntime·persistentallocに変更されました。これは、これらのデータが永続的であり、GCの対象外とすることで、GCのオーバーヘッドをさらに削減できることを意味します。

関連リンク

参考にした情報源リンク