[インデックス 15178] ファイルの概要
このコミットは、Goランタイムにおけるハッシュマップ(Hmap)のガベージコレクション(GC)を、より「正確(precise)」にするための重要な変更を導入しています。これにより、GCがハッシュマップの内部構造を正確に理解し、マップ内のキーや値、そして内部のサブテーブルが指すオブジェクトをより効率的かつ正確に識別・マークできるようになります。結果として、メモリの再利用効率が向上し、アプリケーションのメモリフットプリントが削減される可能性があります。
コミット
commit 1e01fba2fc9a8824fee899956ead1518eae9613b
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date: Fri Feb 8 16:00:33 2013 -0500
runtime: precise garbage collection of hashmaps
R=golang-dev, rsc
CC=dave, dvyukov, golang-dev, minux.ma, remyoudompheng
https://golang.org/cl/7252047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1e01fba2fc9a8824fee899956ead1518eae9613b
元コミット内容
runtime: precise garbage collection of hashmaps
R=golang-dev, rsc
CC=dave, dvyukov, golang-dev, minux.ma, remyoudompheng
https://golang.org/cl/7252047
変更の背景
Goのガベージコレクタは、プログラムが使用しなくなったメモリを自動的に解放する役割を担っています。初期のGoのGCは、特定のデータ構造(特に複雑な内部構造を持つもの)に対して「保守的(conservative)」なアプローチを取ることがありました。保守的なGCでは、メモリ領域内にポインタのように見える値が存在する場合、それが実際にポインタであるかどうかを厳密に判断せず、安全のためにその領域を「生きている」とみなして解放しないことがあります。これは、誤って使用中のメモリを解放してしまうリスクを避けるための安全策ですが、結果として不要なメモリが解放されずに残り、メモリリークやメモリ使用量の増加につながる可能性があります。
ハッシュマップは、内部的に複雑なデータ構造(バケット、サブテーブル、キーと値のデータなど)を持つため、保守的なGCの対象となりやすい構造の一つでした。このコミットの背景には、ハッシュマップのGCを保守的から「正確(precise)」に移行することで、メモリ管理の効率を向上させ、より多くの不要なメモリを確実に回収するという目的があります。これにより、Goアプリケーションのメモリフットプリントを削減し、全体的なパフォーマンスを改善することが期待されます。
前提知識の解説
ガベージコレクション (GC)
- 目的: プログラムが動的に確保したメモリのうち、もはや到達不可能(参照されなくなった)になったものを自動的に解放し、メモリリークを防ぎ、開発者のメモリ管理の負担を軽減します。
- GoのGC: Goは、並行(concurrent)かつ三色マーク&スイープ(tri-color mark-and-sweep)アルゴリズムをベースとしたGCを採用しています。これは、GCがアプリケーションの実行と並行して動作し、アプリケーションの一時停止時間(stop-the-world時間)を最小限に抑えることを目指しています。
- マーク&スイープ:
- マークフェーズ: GCは、ルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトを「生きている(live)」とマークします。
- スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが占めるメモリ領域を「死んでいる(dead)」と判断し、解放して再利用可能な状態にします。
- 保守的GC vs. 正確GC:
- 保守的GC: メモリ領域内のビットパターンがポインタのように見える場合、それが実際にポインタであるかどうかを厳密に判断せず、安全のためにポインタであると仮定します。これにより、誤って生きているオブジェクトを解放するリスクは減りますが、実際にはポインタではないデータがポインタと誤認され、不要なメモリが解放されない可能性があります。
- 正確GC: データ構造のレイアウトを正確に把握しており、どのメモリ位置がポインタを格納しているかを正確に識別できます。これにより、ポインタではないデータを誤ってポインタとみなすことがなくなり、より効率的かつ完全に不要なメモリを回収できます。
Goのハッシュマップ (Hmap)
Goのマップは、内部的にハッシュテーブルとして実装されています。その構造は、キーと値のペアを格納する「バケット」と、バケットが満杯になった場合にリンクされる「オーバーフローバケット」、そしてマップが大きくなった場合にバケットの集合を管理する「サブテーブル」など、複数の層から構成されています。これらの内部構造は、GCがマップを正確にスキャンするために理解する必要があるポインタを含んでいます。
GCプログラム
GoのGCは、異なる型のオブジェクトをスキャンするために「GCプログラム」と呼ばれる一連の命令を使用します。これは、特定のデータ構造内のどこにポインタが存在するかをGCに指示するものです。例えば、構造体の場合、各フィールドがポインタであるかどうかに応じて、GCは適切なスキャン命令を実行します。
技術的詳細
このコミットの核心は、Goランタイムがハッシュマップの内部構造を正確に走査し、その中に含まれるすべてのポインタ(キー、値、および内部サブテーブルへの参照)を識別できるようにすることです。
-
ハッシュマップイテレータの導入:
src/pkg/runtime/hashmap.hとsrc/pkg/runtime/hashmap.cに、GC専用の新しいイテレータ構造体hash_gciterとhash_gciter_data、および関連する関数hash_gciter_initとhash_gciter_nextが追加されました。hash_gciter_initは、GCがマップのスキャンを開始する際にイテレータを初期化します。hash_gciter_nextは、マップの内部を深さ優先で走査し、次のサブテーブル、キーデータ、または値データを返します。これにより、GCはマップの複雑な階層構造を効率的にナビゲートできます。- このイテレータは、マップのキーや値がポインタを含むかどうか(
IndirectKey,IndirectValフラグ)も考慮し、それに応じてポインタのマーク方法を調整します。
-
GCスキャンロジックの更新 (
mgc0.c):src/pkg/runtime/mgc0.cは、GoのGCの主要なスキャンロジックを含むファイルです。このファイルが大幅に更新され、ハッシュマップの正確なGCをサポートするようになりました。markonly関数の追加: これは、特定のオブジェクトがGCアリーナ内にあり、まだマークされていない場合にのみマークするヘルパー関数です。これにより、GCはオブジェクトの開始アドレスを正確に特定し、そのビットマップを更新してマーク済みとして記録できます。GC_MAP_NEXT命令の追加: GCプログラムに新しい命令GC_MAP_NEXTが追加されました。これは、GCがハッシュマップの要素をスキャンする際に使用されます。scanblock関数の変更:scanblockは、GCがメモリブロックをスキャンする主要な関数です。この関数内で、マップ型のオブジェクトが検出された場合、新しいハッシュマップイテレータ (hash_gciter) が初期化されます。mapProgという新しいGCプログラムが導入され、マップのスキャンに特化しています。GC_MAP_NEXT命令が実行されると、hash_gciter_nextを呼び出してマップの次の要素(サブテーブル、キー、値)を取得します。- 取得した要素がポインタを含む場合、それらはGCの内部バッファ(
objbufまたはptrbuf)に追加され、後続のGCサイクルでさらにスキャンされます。特に、サブテーブル自体もGCの対象としてマークされます。
flushobjbuf関数の追加:PtrTargetと同様に、Obj(オブジェクトのポインタとサイズ、型情報を含む構造体)をバッファリングし、必要に応じてフラッシュするためのflushobjbuf関数が追加されました。これにより、GCはマップから見つかった多数のオブジェクトを効率的に処理できます。
これらの変更により、GCはハッシュマップの内部構造を「ブラックボックス」として扱うのではなく、その内容を正確に検査し、到達可能なすべてのポインタをマークできるようになりました。これにより、マップが不要になった際に、その内部で保持されていたオブジェクトも適切に解放されるようになり、メモリ使用効率が向上します。
コアとなるコードの変更箇所
src/pkg/runtime/hashmap.c
hash_subtable_new関数内で、メモリ割り当てがmallocからruntime·mallocgcに変更されました。これは、ハッシュマップのサブテーブルがGCによって管理されるヒープメモリに割り当てられることを意味します。hash_gciter_init関数が追加されました。これは、GCがハッシュマップをスキャンする際に使用するイテレータを初期化します。マップがポインタを含まない場合はfalseを返します。hash_gciter_next関数が追加されました。これは、イテレータを進め、次のサブテーブル、キーデータ、または値データを返します。マップの内部構造(サブテーブルのネストを含む)を再帰的に走査します。
src/pkg/runtime/hashmap.h
mallocマクロの定義が削除されました。- GCイテレータのための新しい構造体
hash_gciterとhash_gciter_dataが定義されました。hash_gciter: イテレータの状態(要素サイズ、フラグ、スタックポインタ、現在のサブテーブル、サブテーブルの状態スタック)を保持します。hash_gciter_data: イテレータが返すデータ(サブテーブルポインタ、キーデータ、値データ、キーと値が間接ポインタであるかどうかのフラグ)を保持します。
hash_gciter_initとhash_gciter_nextの関数プロトタイプが追加されました。
src/pkg/runtime/mgc0.c
hashmap.hがインクルードされました。GC_MAP_NEXTがGC命令の列挙型に追加されました。markonly静的関数が追加されました。これは、与えられたオブジェクトがGCアリーナ内にあり、まだマークされていない場合に、そのオブジェクトをマークします。BufferList構造体にObj obj[IntermediateBufferCapacity];が追加されました。これは、GCスキャン中に見つかったオブジェクト(ポインタとサイズ、型情報)を一時的にバッファリングするためのものです。flushobjbuf関数が追加されました。これは、objbufにバッファリングされたオブジェクトをGCのワークバッファにフラッシュする役割を担います。mapProgという新しいGCプログラムが定義されました。これは、ハッシュマップの要素をスキャンするための命令シーケンスです。scanblock関数が大幅に修正されました。- マップ型のオブジェクトが検出された場合 (
case TypeInfo_Map)、hash_gciter_initを呼び出してマップイテレータを初期化し、GCプログラムカウンタ (pc) をmapProgに設定してマップのスキャンを開始します。 GC_MAP_NEXT命令の処理が追加されました。このロジックはhash_gciter_nextを繰り返し呼び出し、マップ内のすべてのサブテーブル、キー、値を走査します。- 走査中に見つかったサブテーブルは
markonlyでマークされます。 - キーや値がポインタを含む場合、それらは
objbufまたはptrbufに追加され、GCによってさらにスキャンされる対象となります。 GC_MAP_PTR命令の処理も更新され、マップオブジェクト自体をマークし、そのマップの正確なスキャンを開始するようになりました。scanblockの終了時にflushobjbufが呼び出され、バッファリングされたすべてのオブジェクトが処理されることが保証されます。
- マップ型のオブジェクトが検出された場合 (
コアとなるコードの解説
このコミットの核となる変更は、Goのガベージコレクタがハッシュマップの内部構造を正確に理解し、その中のポインタを識別できるようにするための新しいメカニズムの導入です。
-
hash_gciterとその関連関数:hash_gciterは、ハッシュマップの内部を走査するための状態を保持する構造体です。これには、現在の要素サイズ、マップのフラグ(キーや値が間接ポインタであるかなど)、現在のサブテーブル、そしてネストされたサブテーブルを処理するためのスタックが含まれます。hash_gciter_init(Hmap *h, struct hash_gciter *it): GCが特定のハッシュマップをスキャンする前に呼び出され、イテレータを初期化します。マップが空であるか、ポインタを含まない場合はfalseを返します。hash_gciter_next(struct hash_gciter *it, struct hash_gciter_data *data): イテレータを次の要素に進めます。この関数は、マップのバケットを走査し、キーと値のペア、またはネストされたサブテーブルを見つけます。見つかった要素の情報はhash_gciter_data構造体に格納されます。data->st: サブテーブルへのポインタ。サブテーブル自体もGCの対象です。data->key_data,data->val_data: キーと値のデータへのポインタ。data->indirectkey,data->indirectval: キーや値が間接ポインタ(つまり、データ自体がポインタではなく、ポインタを指すポインタ)であるかどうかを示します。これは、GCが正しい深さでポインタを追跡するために重要です。
-
scanblockにおけるマップスキャンロジック:scanblock関数は、GCがメモリブロックを走査し、到達可能なオブジェクトをマークする主要なループです。- マップオブジェクトの検出:
scanblockがTypeInfo_Map型のオブジェクト(つまり、Goのマップ)に遭遇すると、そのマップの正確なスキャンを開始します。 hash_gciter_initの呼び出し: まず、hash_gciter_initを呼び出して、このマップのためのGCイテレータを初期化します。mapProgの使用: マップのスキャンには、mapProgという専用のGCプログラムが使用されます。このプログラムは、GC_MAP_NEXT命令を含んでいます。GC_MAP_NEXTの処理:scanblockのメインループ内でGC_MAP_NEXT命令が実行されると、hash_gciter_nextが繰り返し呼び出されます。hash_gciter_nextがサブテーブルを返した場合 (d.st != nil)、そのサブテーブル自体がmarkonly関数によってマークされます。hash_gciter_nextがキーまたは値を返した場合 (d.key_data != nilまたはd.val_data != nil)、GCはそれらがポインタを含むかどうかをチェックします(KindNoPointersフラグとindirectkey/indirectvalフラグを使用)。- ポインタを含むキーや値は、GCの内部バッファ(
objbufまたはptrbuf)に追加されます。これらのバッファは、後続のGCサイクルでさらにスキャンされ、それらが指すオブジェクトもマークされます。
flushobjbuf:objbufは、scanblockの実行中に見つかったオブジェクト(ポインタとサイズ、型情報)を一時的に保持するバッファです。このバッファが満杯になった場合や、scanblockの終了時には、flushobjbufが呼び出されて、バッファ内のすべてのオブジェクトがGCのメインワークキューにフラッシュされます。これにより、GCはこれらのオブジェクトをさらに処理し、それらが指すオブジェクトをマークできます。
これらの変更により、GoのGCはハッシュマップの内部構造を「正確」にスキャンできるようになり、マップが不要になった際に、その内部で保持されていたすべての到達不可能なオブジェクトを確実に解放できるようになりました。これは、メモリ使用量の最適化とGCパフォーマンスの向上に貢献します。
関連リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事:
- Go's Garbage Collector: A Brief History (Go 1.5 GCに関する記事ですが、GCの進化の背景を理解するのに役立ちます)
- The Go Programming Language Specification - Map types (Goのマップの言語仕様)
- Goのランタイムソースコード:
参考にした情報源リンク
- Goのソースコード(特に
src/pkg/runtime/hashmap.c,src/pkg/runtime/hashmap.h,src/pkg/runtime/mgc0.cの変更点) - Goのガベージコレクションに関する一般的な知識
- ハッシュテーブルのデータ構造に関する一般的な知識
- コミットメッセージと関連するGo CL (Change List) の情報 (
https://golang.org/cl/7252047)