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

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

このコミットは、Goランタイムのhashmap.cファイルにおけるメモリ割り当て(mallocgc())とガベージコレクション(GC)の相互作用をデバッグするためのオプション機能を追加するものです。具体的には、新しいデバッグ定数checkgcを導入し、この値が非ゼロの場合にhashmap.cからのmallocgc()呼び出しがガベージコレクションを強制的に開始するように変更されています。これにより、ハッシュマップ操作中のメモリ割り当てがGCに与える影響を詳細に調査できるようになります。

コミット

commit bf1f46180ee348d2d59bebfeda0314450fbcd893
Author: Jan Ziak <0xe2.0x9a.0x9b@gmail.com>
Date:   Mon Mar 25 21:35:46 2013 +0100

    runtime: optionally check all allocations in hashmap.c
    
    Adds the new debugging constant 'checkgc'. If its value is non-zero
    all calls to mallocgc() from hashmap.c will start a garbage collection.
    
    Fixes #5074.
    
    R=golang-dev, khr
    CC=golang-dev, rsc
    https://golang.org/cl/7663051

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

https://github.com/golang/go/commit/bf1f46180ee348d2d59bebfeda0314450fbcd893

元コミット内容

runtime: optionally check all allocations in hashmap.c

Adds the new debugging constant 'checkgc'. If its value is non-zero
all calls to mallocgc() from hashmap.c will start a garbage collection.

Fixes #5074.

R=golang-dev, khr
CC=golang-dev, rsc
https://golang.org/cl/7663051

変更の背景

この変更の背景には、Goランタイムのハッシュマップ(map型)の実装におけるメモリ管理とガベージコレクションの相互作用に関するデバッグの必要性があります。ハッシュマップは動的にサイズが変更され、要素の追加や削除、リサイズ(再ハッシュ)の際に頻繁にメモリの割り当てや解放が行われます。これらの操作がガベージコレクタの動作にどのような影響を与えるか、特にGCのタイミングや効率に問題がないかを検証することは、ランタイムの安定性とパフォーマンスを確保する上で重要です。

コミットメッセージにあるFixes #5074は、特定のバグや問題の修正を示唆していますが、Goの公式リポジトリではこのIssue番号に関する情報は見つかりませんでした。しかし、一般的にこのようなデバッグ機能の追加は、特定の条件下で発生するメモリ関連のバグ(例: メモリリーク、GCの遅延、GCによるパフォーマンス低下など)を特定し、再現性を高めるために行われます。hashmap.c内のmallocgc()呼び出しの直前にGCを強制的に実行することで、メモリ割り当てとGCの競合状態や、GCがメモリを解放するタイミングがハッシュマップの動作に与える影響を詳細に調査できるようになります。

前提知識の解説

Goのガベージコレクション (GC)

Go言語は、自動メモリ管理のためにガベージコレクタ(GC)を内蔵しています。GoのGCは、主に並行マーク&スイープ方式を採用しており、プログラムの実行と並行して不要になったメモリ領域を自動的に回収します。これにより、開発者は手動でのメモリ管理から解放され、メモリリークなどのバグを減らすことができます。

  • mallocgc(): Goランタイム内部でメモリを割り当てるための関数です。Goプログラムが新しいオブジェクトを作成したり、データ構造を拡張したりする際に、この関数が呼び出されてヒープからメモリが確保されます。mallocgcは、必要に応じてGCをトリガーする役割も持っています。
  • mstats.heap_alloc: Goランタイムが現在までにヒープに割り当てたメモリの総量を示す統計情報です。
  • mstats.next_gc: 次のGCがトリガーされるヒープ割り当ての閾値を示す統計情報です。mstats.heap_allocmstats.next_gcを超えると、GCが実行されます。

Goのハッシュマップ (Map)

Goのmap型は、キーと値のペアを格納する組み込みのデータ構造であり、内部的にはハッシュテーブルとして実装されています。ハッシュマップは、高速なキーによる値の検索、挿入、削除を提供します。

  • 動的なサイズ変更: ハッシュマップは、要素の増減に応じて内部のバケット数を動的に調整します。要素数が一定の閾値を超えると、より大きなバケット配列にデータを再配置する「再ハッシュ(evacuation/grow)」処理が行われます。
  • メモリ割り当て: 新しいバケット配列の確保や、オーバーフローバケットの割り当てなど、ハッシュマップの操作中に頻繁にメモリ割り当てが発生します。これらのメモリ割り当ては、runtime·mallocgcを通じて行われます。
  • BUCKETSIZE: ハッシュマップの各バケットが保持できるキー/値ペアの数。

技術的詳細

このコミットで導入されたcheckgc定数は、Goランタイムのデバッグビルドにおいて、ハッシュマップ関連のメモリ割り当てがガベージコレクションに与える影響を詳細に調査するためのメカニズムを提供します。

checkgcは、src/pkg/runtime/hashmap.c内で定義されるデバッグ定数であり、デフォルトでは0(無効)に設定されています。しかし、docheck(別のデバッグ定数)が有効になっている場合は、checkgcも自動的に有効になります。

enum
{
	docheck = 0,  // check invariants before and after every op.  Slow!!!
	debug = 0,    // print every operation
	checkgc = 0 || docheck,  // check interaction of mallocgc() with the garbage collector
};

checkgcが非ゼロ(有効)の場合、hashmap.c内の特定のruntime·mallocgc呼び出しの直前に、mstats.next_gc = mstats.heap_alloc;という行が挿入されます。この行の目的は、次のガベージコレクションを強制的にトリガーすることです。

通常、GoのGCはmstats.heap_allocmstats.next_gcの閾値を超えたときに実行されます。mstats.next_gc = mstats.heap_alloc;と設定することで、現在のヒープ割り当て量(mstats.heap_alloc)を次のGCの閾値として設定します。これにより、その直後に行われるruntime·mallocgc呼び出しが、たとえ少量であっても、mstats.heap_allocmstats.next_gcより大きくするため、GCが即座にトリガーされることになります。

このメカニズムにより、開発者はハッシュマップのメモリ割り当てがGCに与える影響を、非常に細かい粒度で観察できるようになります。例えば、ハッシュマップの拡張(hash_grow)や新しいバケットの割り当て(evacuatehash_insert)といった操作の直後にGCがどのように動作するか、GCが完了するまでの時間、メモリ使用量の変化などを詳細にプロファイリングすることが可能になります。これは、特定のメモリ関連のバグの再現や、GCのパフォーマンスチューニングにおいて非常に有用なデバッグ手法です。

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

--- a/src/pkg/runtime/hashmap.c
+++ b/src/pkg/runtime/hashmap.c
@@ -126,6 +126,7 @@ enum
 {
 	docheck = 0,  // check invariants before and after every op.  Slow!!!
 	debug = 0,    // print every operation
+\tcheckgc = 0 || docheck,  // check interaction of mallocgc() with the garbage collector
 };
 static void
 check(MapType *t, Hmap *h)
@@ -253,6 +254,7 @@ hash_init(MapType *t, Hmap *h, uint32 hint)\n \n \t// allocate initial hash table\n \t// If hint is large zeroing this memory could take a while.\n+\tif(checkgc) mstats.next_gc = mstats.heap_alloc;\n \tbuckets = runtime·mallocgc(bucketsize << B, 0, 1, 0);\n \tfor(i = 0; i < (uintptr)1 << B; i++) {\n \t\tb = (Bucket*)(buckets + i * bucketsize);\n@@ -322,6 +324,7 @@ evacuate(MapType *t, Hmap *h, uintptr oldbucket)\n \t\t\t\t// the B'th bit of the hash in this case.\n \t\t\t\tif((hash & newbit) == 0) {\n \t\t\t\t\tif(xi == BUCKETSIZE) {\n+\t\t\t\t\t\tif(checkgc) mstats.next_gc = mstats.heap_alloc;\n \t\t\t\t\t\tnewx = runtime·mallocgc(h->bucketsize, 0, 1, 0);\n \t\t\t\t\t\tclearbucket(newx);\n \t\t\t\t\t\tx->overflow = newx;\n@@ -346,6 +349,7 @@ evacuate(MapType *t, Hmap *h, uintptr oldbucket)\n \t\t\t\t\txv += h->valuesize;\n \t\t\t\t} else {\n \t\t\t\t\tif(yi == BUCKETSIZE) {\n+\t\t\t\t\t\tif(checkgc) mstats.next_gc = mstats.heap_alloc;\n \t\t\t\t\t\tnewy = runtime·mallocgc(h->bucketsize, 0, 1, 0);\n \t\t\t\t\t\tclearbucket(newy);\n \t\t\t\t\t\ty->overflow = newy;\n@@ -441,6 +445,7 @@ hash_grow(MapType *t, Hmap *h)\n \t\truntime·throw(\"evacuation not done in time\");\n \told_buckets = h->buckets;\n \t// NOTE: this could be a big malloc, but since we don't need zeroing it is probably fast.\n+\tif(checkgc) mstats.next_gc = mstats.heap_alloc;\n \tnew_buckets = runtime·mallocgc(h->bucketsize << (h->B + 1), 0, 1, 0);\n \tflags = (h->flags & ~(Iterator | OldIterator));\n \tif((h->flags & Iterator) != 0) {\n@@ -611,6 +616,7 @@ hash_insert(MapType *t, Hmap *h, void *key, void *value)\n \n \tif(inserti == nil) {\n \t\t// all current buckets are full, allocate a new one.\n+\t\tif(checkgc) mstats.next_gc = mstats.heap_alloc;\n \t\tnewb = runtime·mallocgc(h->bucketsize, 0, 1, 0);\n \t\tclearbucket(newb);\n \t\tb->overflow = newb;\n@@ -621,11 +627,13 @@ hash_insert(MapType *t, Hmap *h, void *key, void *value)\n \n \t// store new key/value at insert position\n \tif((h->flags & IndirectKey) != 0) {\n+\t\tif(checkgc) mstats.next_gc = mstats.heap_alloc;\n \t\tkmem = runtime·mallocgc(t->key->size, 0, 1, 0);\n \t\t*(byte**)insertk = kmem;\n \t\tinsertk = kmem;\n \t}\n \tif((h->flags & IndirectValue) != 0) {\n+\t\tif(checkgc) mstats.next_gc = mstats.heap_alloc;\n \t\tvmem = runtime·mallocgc(t->elem->size, 0, 1, 0);\n \t\t*(byte**)insertv = vmem;\n \t\tinsertv = vmem;\n```

## コアとなるコードの解説

このコミットは、`src/pkg/runtime/hashmap.c`ファイルに以下の変更を加えています。

1.  **`checkgc`定数の追加**:
    ```c
    checkgc = 0 || docheck,  // check interaction of mallocgc() with the garbage collector
    ```
    `enum`ブロック内に新しいデバッグ定数`checkgc`が追加されました。デフォルト値は`0`ですが、もし`docheck`が`1`であれば`checkgc`も`1`になります。これは、ハッシュマップの不変条件チェック(`docheck`)が有効な場合に、GCとの相互作用チェックも同時に有効にするためのものです。

2.  **`hash_init`関数内でのGC強制**:
    ```c
    if(checkgc) mstats.next_gc = mstats.heap_alloc;
    buckets = runtime·mallocgc(bucketsize << B, 0, 1, 0);
    ```
    `hash_init`関数は、新しいハッシュマップが初期化される際に呼び出されます。この関数内で最初のハッシュテーブル(バケット配列)が`runtime·mallocgc`によって割り当てられる直前に、`checkgc`が有効な場合に`mstats.next_gc = mstats.heap_alloc;`が挿入されます。これにより、ハッシュマップの初期化時におけるメモリ割り当ての直後にGCが強制的に実行されるようになります。

3.  **`evacuate`関数内でのGC強制**:
    ```c
    if(checkgc) mstats.next_gc = mstats.heap_alloc;
    newx = runtime·mallocgc(h->bucketsize, 0, 1, 0);
    ```
    ```c
    if(checkgc) mstats.next_gc = mstats.heap_alloc;
    newy = runtime·mallocgc(h->bucketsize, 0, 1, 0);
    ```
    `evacuate`関数は、ハッシュマップが再ハッシュ(リサイズ)される際に、古いバケットから新しいバケットへ要素を移動させる処理を行います。この処理中に新しいオーバーフローバケットが`runtime·mallocgc`によって割り当てられる箇所が2箇所あり、それぞれの直前に`checkgc`が有効な場合にGCが強制されるコードが追加されています。これにより、再ハッシュ中のメモリ割り当てとGCの相互作用をデバッグできます。

4.  **`hash_grow`関数内でのGC強制**:
    ```c
    if(checkgc) mstats.next_gc = mstats.heap_alloc;
    new_buckets = runtime·mallocgc(h->bucketsize << (h->B + 1), 0, 1, 0);
    ```
    `hash_grow`関数は、ハッシュマップのサイズが拡張される際に、より大きな新しいバケット配列を割り当てるために呼び出されます。この新しいバケット配列が`runtime·mallocgc`によって割り当てられる直前に、`checkgc`が有効な場合にGCが強制されるコードが追加されています。これは、ハッシュマップの最も大きなメモリ割り当ての一つである拡張操作におけるGCの挙動を調査するために重要です。

5.  **`hash_insert`関数内でのGC強制**:
    ```c
    if(checkgc) mstats.next_gc = mstats.heap_alloc;
    newb = runtime·mallocgc(h->bucketsize, 0, 1, 0);
    ```
    ```c
    if(checkgc) mstats.next_gc = mstats.heap_alloc;
    kmem = runtime·mallocgc(t->key->size, 0, 1, 0);
    ```
    ```c
    if(checkgc) mstats.next_gc = mstats.heap_alloc;
    vmem = runtime·mallocgc(t->elem->size, 0, 1, 0);
    ```
    `hash_insert`関数は、ハッシュマップに新しいキーと値のペアを挿入する際に呼び出されます。この関数内で、新しいオーバーフローバケットの割り当て、および間接的なキーや値のメモリ割り当て(ポインタを介してキーや値が格納される場合)が行われる直前に、`checkgc`が有効な場合にGCが強制されるコードが追加されています。これにより、個々の要素挿入時のメモリ割り当てとGCの相互作用を詳細にデバッグできます。

これらの変更はすべて、`checkgc`というデバッグフラグによって制御されており、通常運用時には無効になっています。これにより、デバッグ時のみGCを強制的に実行し、ハッシュマップのメモリ管理とGCの相互作用に関する潜在的な問題を特定・分析することが可能になります。

## 関連リンク

*   Go CL (Code Review) リンク: [https://golang.org/cl/7663051](https://golang.org/cl/7663051)
*   関連する可能性のあるIssue: #5074 (ただし、Goの公式リポジトリではこのIssue番号に関する情報は見つかりませんでした。)

## 参考にした情報源リンク

*   Go CL 7663051: [https://golang.org/cl/7663051](https://golang.org/cl/7663051)
*   Goのガベージコレクションに関する一般的な情報源 (例: Go公式ドキュメント、Goのメモリ管理に関するブログ記事など)
*   Goの`map`実装に関する一般的な情報源 (例: Go公式ブログの`map`に関する記事、Goのソースコード解説など)
*   Go Issue 5074については、Web検索では関連情報を見つけることができませんでした。