[インデックス 16419] ファイルの概要
このコミットは、以前に導入されたコミット CL 9805043 / 776aba85ece8
を元に戻す(undo)ものです。元に戻されたコミットは、Goランタイムに persistentalloc()
というヘルパー関数を導入し、シンボルテーブル(symtab)の割り当てなどに利用することで、ビルド時間の短縮と初期ヒープサイズの削減を目指していました。しかし、この persistentalloc()
の導入が amd64
アーキテクチャで複数の障害を引き起こしたため、安定性確保のためにその変更が取り消されました。
コミット
commit 828c68f8d80a642d89cc17e04aeb0116c8bce4ae
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue May 28 11:14:39 2013 +0400
undo CL 9805043 / 776aba85ece8
multiple failures on amd64
««« original CL description
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
»»»
R=golang-dev
CC=golang-dev
https://golang.org/cl/9822043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/828c68f8d80a642d89cc17e04aeb0116c8bce4ae
元コミット内容
このコミットによって元に戻された CL 9805043
は、Goランタイムのメモリ管理を最適化することを目的としていました。具体的には、persistentalloc()
という新しいヘルパー関数を導入しました。この関数は、SysAlloc()
(システムコールによるメモリ割り当て)のラッパーとして機能し、特に小さなチャンクのメモリをキャッシュして効率的に割り当てることを意図していました。
主な目的は以下の通りです。
- シンボルテーブル(symtab)の割り当て:
buildfuncs
の処理において、シンボルテーブルのウォーク回数を4回から3回に削減し、大規模なバイナリでのビルド時間を10msから7.5msに短縮し、初期ヒープサイズを680KB削減する効果が見込まれていました。 - 汎用的な永続的データ割り当て: 型情報(type info)、itab(インターフェーステーブル)の割り当てにも利用できるとされていました。
- GC(ガベージコレクション)の最適化: GC内の同様の割り当てパターンを
persistentalloc()
に置き換える可能性が示唆されていました。 - FixAllocの改善:
FixAlloc
が128KB単位でメモリを割り当てるという「積極的すぎる」挙動を改善するためにも利用できるとされていました。
persistentalloc()
は、割り当てられたメモリが解放されない(永続的である)ことを前提としており、GCの対象外となるデータに適していました。
変更の背景
CL 9805043
で導入された persistentalloc()
関数は、パフォーマンスの向上とメモリ使用量の削減を目指したものでしたが、このコミットのログにあるように「multiple failures on amd64
」(amd64アーキテクチャでの複数の障害)が発生したため、その変更を元に戻すことが決定されました。
具体的な障害の内容はこのコミットメッセージからは読み取れませんが、ランタイムのメモリ割り当てのような低レベルな変更は、特定のアーキテクチャや環境において予期せぬクラッシュ、メモリリーク、データ破損などの深刻な問題を引き起こす可能性があります。特に amd64
はGoが広く利用される主要なアーキテクチャであるため、そこで安定性の問題が発生したことは、その変更を即座にロールバックする十分な理由となります。この決定は、パフォーマンス最適化よりもランタイムの安定性と信頼性を優先するというGo開発チームの姿勢を示しています。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGoランタイムおよびメモリ管理に関する知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルなシステムです。これには、スケジューラ(ゴルーチンの管理)、ガベージコレクタ(GC)、メモリ割り当て、システムコールインターフェースなどが含まれます。Goプログラムは、OS上で直接実行されるのではなく、このランタイム上で動作します。
-
SysAlloc()
: Goランタイムがオペレーティングシステムから直接メモリを要求するために使用する関数です。これは通常、大きなメモリ領域をOSから取得する際に利用されます。persistentalloc()
は、このSysAlloc()
を基盤として、より小さなチャンクを効率的に管理しようとしたものです。 -
persistentalloc()
(元コミットで導入された機能): 元コミットで導入された、永続的な(GCの対象とならない)小さなメモリチャンクを効率的に割り当てるためのヘルパー関数です。SysAlloc()
で取得した大きなメモリブロックをキャッシュとして利用し、そこから小さな要求に応えることで、システムコール頻度を減らし、メモリの断片化を抑えることを目指していました。主に、プログラムの実行中に内容が変化しない、あるいはGCのオーバーヘッドを避けたいデータ(例: シンボルテーブル、型情報)のために設計されました。 -
シンボルテーブル (
symtab
): Goバイナリに含まれる重要なデータ構造の一つです。関数名、変数名、ファイル名、行番号などのシンボル情報が格納されており、デバッグ、プロファイリング、スタックトレースの生成、リフレクションなどのランタイム機能に不可欠です。Goのバイナリサイズに大きく影響する要素でもあります。 -
mallocgc()
: Goランタイムの主要なメモリ割り当て関数です。Goのガベージコレクタによって管理されるヒープメモリからメモリを割り当てます。割り当てられたメモリはGCの対象となり、不要になった時点で自動的に解放されます。 -
FlagNoPointers
:mallocgc()
に渡されるフラグの一つです。このフラグが設定されている場合、割り当てられるメモリ領域にはポインタが含まれないことをGCに伝えます。これにより、GCはこの領域をスキャンする必要がなくなり、GCのオーバーヘッドを削減できます。シンボルテーブルや一部の型情報など、ポインタを含まないデータ構造の割り当てに適しています。 -
hugestring
: このコミットでsymtab.c
に導入された新しいメカニズムです。persistentalloc()
の代替として、シンボルテーブル内の文字列(関数名、ファイル名など)を効率的に管理するために使用されます。これは、すべての文字列を一つの大きな連続したメモリブロック(hugestring
)に格納し、個々の文字列はそのブロック内のスライスとして表現するアプローチです。これにより、文字列ごとの個別のメモリ割り当てを減らし、メモリの局所性を高めることができます。 -
amd64
: x86-64アーキテクチャのことで、現代のほとんどのデスクトップPCやサーバーで使用されている64ビットプロセッサアーキテクチャです。Goはクロスコンパイルが可能ですが、特定のアーキテクチャに依存する低レベルなランタイムの変更は、そのアーキテクチャ固有のバグを引き起こす可能性があります。
技術的詳細
このコミットは、persistentalloc()
の導入を取り消し、その影響を受けた箇所を元の状態に戻すか、代替のメカニズムを導入しています。
-
src/pkg/runtime/malloc.goc
およびsrc/pkg/runtime/malloc.h
からpersistentalloc()
の削除:malloc.goc
からpersistentalloc()
関数の実装全体が削除されました。これには、persistent
構造体(ロック、ポインタ、エンドポインタを含む)、PersistentAllocChunk
およびPersistentAllocMaxBlock
定義、そして実際のメモリ割り当てロジックが含まれます。malloc.h
からruntime·persistentalloc
の関数宣言が削除されました。- これにより、
persistentalloc()
を利用したメモリ割り当てはGoランタイムから完全に排除されました。
-
src/pkg/runtime/symtab.c
の変更: このファイルはシンボルテーブルの構築に関連しており、元コミットでpersistentalloc()
が利用されていました。このコミットでは、persistentalloc()
の使用を中止し、代わりにmallocgc()
と新しいhugestring
メカニズムを導入しています。-
func
およびfname
配列の割り当て: 元コミットではfunc
とfname
の配列をruntime·persistentalloc
で割り当てていましたが、このコミットではruntime·mallocgc
に戻されました。// 変更前 (元コミット) // func = runtime·persistentalloc((nfunc+1)*sizeof func[0], 0); // fname = runtime·persistentalloc(nfname*sizeof fname[0], 0); // 変更後 (このコミット) func = runtime·mallocgc((nfunc+1)*sizeof func[0], FlagNoPointers, 0, 1); fname = runtime·mallocgc(nfname*sizeof fname[0], FlagNoPointers, 0, 1);
ここで注目すべきは、
mallocgc
にFlagNoPointers
フラグが渡されている点です。これは、これらの配列がポインタを含まない(あるいは、含まれるポインタがGCによってスキャンされる必要がない)ことをGCに伝え、GCの効率を向上させます。 -
hugestring
メカニズムの導入: シンボルテーブル内の文字列(関数名、ファイル名など)の管理方法が大きく変更されました。元コミットではこれらの文字列もpersistentalloc
で個別に割り当てていましたが、このコミットではhugestring
という単一の大きなメモリブロックにすべての文字列を格納する方式が導入されました。static String hugestring; static int32 hugestring_len;
gostringn
関数が変更され、文字列をhugestring
に追加するようになりました。// 変更前 (元コミット) // s.str = runtime·persistentalloc(l, 1); // runtime·memmove(s.str, p, l); // 変更後 (このコミット) if(hugestring.str == nil) { hugestring_len += l; return runtime·emptystring; } s.str = hugestring.str + hugestring.len; hugestring.len += s.len; runtime·memmove(s.str, p, l);
この変更により、
gostringn
は2つのパスで動作します。- パス1 (
hugestring.str == nil
の場合):hugestring
の総長 (hugestring_len
) を計算します。この時点では実際のメモリ割り当ては行いません。 - パス2 (
hugestring.str != nil
の場合):hugestring_len
で計算されたサイズに基づいてhugestring
のメモリをmallocgc
で一度に割り当て、その中にすべての文字列データをコピーしていきます。
// buildfuncs 関数内の変更 // ... 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 // ... if(hugestring.len != hugestring_len) runtime·throw("buildfunc: problem in initialization procedure");
この二段階の割り当て戦略により、文字列ごとに個別の
SysAlloc
やpersistentalloc
を呼び出すオーバーヘッドを回避し、メモリの局所性を高めることができます。また、FlagNoPointers
を使用することで、GCのスキャン対象から外すことができ、パフォーマンスに寄与します。 - パス1 (
-
このコミットは、persistentalloc()
の導入が引き起こした安定性の問題を解決するために、その機能を完全に削除し、影響を受けたシンボルテーブルのメモリ割り当てロジックを、mallocgc()
と新しい hugestring
メカニズムを用いた、より堅牢で効率的な方法に置き換えたものです。
コアとなるコードの変更箇所
src/pkg/runtime/malloc.goc
persistent
構造体、PersistentAllocChunk
、PersistentAllocMaxBlock
の定義が削除されました。runtime·persistentalloc
関数の実装全体(約47行)が削除されました。
src/pkg/runtime/malloc.h
void* runtime·persistentalloc(uintptr size, uintptr align);
の関数宣言が削除されました。
src/pkg/runtime/symtab.c
static String hugestring;
とstatic int32 hugestring_len;
が追加されました。gostringn
関数内で、runtime·persistentalloc
の呼び出しが削除され、hugestring
を利用した文字列のコピーロジックに置き換えられました。buildfuncs
関数内で、func
とfname
の割り当てがruntime·persistentalloc
からruntime·mallocgc
(FlagNoPointers
付き) に変更されました。buildfuncs
関数内で、walksymtab(dosrcline)
が2回呼び出されるようになり、1回目でhugestring_len
を計算し、2回目で実際にhugestring
に文字列を格納する二段階の処理が導入されました。dosrcline
関数内にif(hugestring.str == nil)
のチェックが追加され、hugestring
の初期化状態に応じて動作が分岐するようになりました。
コアとなるコードの解説
src/pkg/runtime/malloc.goc
および src/pkg/runtime/malloc.h
の変更
これらのファイルからの persistentalloc
の削除は、この機能がGoランタイムのコアメモリ管理から完全に撤回されたことを意味します。これは、amd64
での障害が、この新しい割り当てメカニズムの根本的な設計または実装上の問題に起因すると判断されたためと考えられます。ランタイムの安定性を最優先するGoの方針に沿った変更です。
src/pkg/runtime/symtab.c
の変更
このファイルでの変更は、persistentalloc
の代替手段を確立することが目的です。
-
hugestring
の導入とgostringn
の変更: シンボルテーブル内の文字列は、バイナリの実行中に頻繁にアクセスされるが、変更されることはほとんどない永続的なデータです。元々persistentalloc
はこのような用途を想定していましたが、問題が発生したため、より制御された方法でメモリを管理する必要があります。hugestring
は、すべての文字列を単一の大きな連続したメモリブロックにまとめることで、メモリの断片化を減らし、キャッシュ効率を高めます。gostringn
関数は、このhugestring
の現在の位置に新しい文字列をコピーし、hugestring
の長さを更新します。これにより、個々の文字列割り当てのオーバーヘッドがなくなります。 -
buildfuncs
における二段階のdosrcline
呼び出し: これはhugestring
メカニズムの肝となる部分です。- 最初の
walksymtab(dosrcline)
パスでは、hugestring.str
がnil
であるため、gostringn
は文字列を実際にコピーせず、単にすべての文字列の合計長 (hugestring_len
) を計算します。 hugestring.str = runtime·mallocgc(hugestring_len, FlagNoPointers, 0, 0);
で、計算された合計長に基づいて、hugestring
用のメモリが一度に割り当てられます。FlagNoPointers
は、このメモリブロックがポインタを含まないためGCスキャンが不要であることを示します。- 二度目の
walksymtab(dosrcline)
パスでは、hugestring.str
が割り当てられているため、gostringn
は実際に文字列データをhugestring
にコピーしていきます。 この二段階アプローチにより、必要なメモリ量を正確に把握した上で一度に割り当てることができ、効率的なメモリ利用とパフォーマンスを実現します。
- 最初の
-
func
およびfname
配列へのmallocgc(..., FlagNoPointers, ...)
の適用:func
とfname
の配列は、関数情報やファイル名情報を格納するもので、これらもまたポインタを含まないか、含まれてもGCが特別に処理する必要がないデータです。persistentalloc
の代わりにmallocgc
を使用しつつFlagNoPointers
を指定することで、GCのオーバーヘッドを最小限に抑えながら、Goの標準的なメモリ管理フレームワークに統合されます。
これらの変更は、persistentalloc
の問題を回避しつつ、シンボルテーブル関連のメモリ割り当てにおいて、パフォーマンスとメモリ効率を維持するための代替策として機能しています。
関連リンク
- 元コミット (CL 9805043): https://golang.org/cl/9805043
- このコミット (CL 9822043): https://golang.org/cl/9822043
参考にした情報源リンク
- Goのメモリ管理に関する一般的な情報:
- The Go Programming Language Specification - Memory Model: https://go.dev/ref/mem
- Go's Memory Allocator: https://go.dev/src/runtime/malloc.go (現在のGoのソースコードは当時のものと異なる可能性がありますが、概念理解に役立ちます)
- Goのシンボルテーブルに関する情報:
- Goのバイナリ構造に関する記事やドキュメント(特定の公式ドキュメントは少ないですが、Goのデバッグやプロファイリングに関する記事で言及されることが多いです)
amd64
アーキテクチャに関する一般的な情報。- Goのコミット履歴とCL (Change List) の概念。