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

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

このコミットは、Goランタイムにおけるハッシュマップのメモリ管理とガベージコレクション(GC)に関する以前の変更(CL 12840043 / 3b9f54db72a1)を取り消すものです。具体的には、ハッシュマップの内部構造(バケット配列)がGCによってスキャンされないようにする試みを元に戻し、その変更がビルドを破壊し、イテレータによって保持されている古いバケット配列がGCによって適切にスキャンされる必要があるという問題に対処しています。

コミット

commit 74e78df107f9e6642ecfc6808f542e2625f7cfb2
Author: Keith Randall <khr@golang.org>
Date:   Tue Aug 13 12:59:39 2013 -0700

    undo CL 12840043 / 3b9f54db72a1
    
    Breaks the build.  Old bucket arrays kept by iterators
    still need to be scanned.
    
    ««« original CL description
    runtime: tell GC not to scan internal hashmap structures.
    We'll do it ourselves via hash_gciter, thanks.
    Fixes bug 6119.
    
    R=golang-dev, dvyukov, cookieo9, rsc
    CC=golang-dev
    https://golang.org/cl/12840043
    »»»
    
    R=golang-dev
    CC=golang-dev
    https://golang.org/cl/12884043

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

https://github.com/golang/go/commit/74e78df107f9e6642ecfc6808f542e2625f7cfb2

元コミット内容

このコミットが取り消した元のコミット(CL 12840043 / 3b9f54db72a1)の目的は以下の通りです。

「runtime: tell GC not to scan internal hashmap structures. We'll do it ourselves via hash_gciter, thanks. Fixes bug 6119.」

これは、Goランタイムのハッシュマップ(map型)の内部構造(特にバケット配列)について、Goのガベージコレクタ(GC)が直接スキャンしないように指示し、代わりにhash_gciterというカスタムのGCイテレータを通じてこれらの構造をGCに認識させることで、バグ6119を修正しようとしたものです。この変更は、runtime·mallocgc関数を呼び出す際にFlagNoPointersフラグを追加することで実現されていました。

変更の背景

元のコミット(CL 12840043)は、GoのハッシュマップのGC処理を最適化し、特定のバグ(バグ6119)を修正することを目的としていました。バグ6119は、ハッシュマップのバケット配列がGCによって適切に処理されないことに関連する可能性があり、FlagNoPointersフラグを使用してGCの挙動を変更することで、この問題を解決しようとしました。

しかし、このコミット(CL 12884043)のコミットメッセージによると、元の変更は「Breaks the build. Old bucket arrays kept by iterators still need to be scanned.」という問題を引き起こしました。これは、ハッシュマップのイテレータが古いバケット配列を参照し続けている場合、GCがこれらの古い配列をスキャンしないと、参照されているオブジェクトが誤って解放されてしまう可能性があることを示唆しています。つまり、FlagNoPointersフラグによってGCがバケット配列をスキャンしなくなった結果、イテレータが保持する古いバケット配列内のポインタがGCによって追跡されなくなり、メモリ安全性や正確なGC処理が損なわれるという重大な問題が発生したため、元の変更を取り消す必要が生じました。

前提知識の解説

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

Goのガベージコレクションは、プログラムが動的に確保したメモリのうち、もはや到達不可能(参照されていない)なオブジェクトを自動的に解放する仕組みです。GoのGCは並行マーク&スイープ方式を採用しており、プログラムの実行と並行してGCが動作することで、アプリケーションの一時停止時間(ストップ・ザ・ワールド時間)を最小限に抑えることを目指しています。

GCは、プログラムが使用しているオブジェクトを「マーク」し、マークされなかったオブジェクトを「スイープ」(解放)します。このマークフェーズでは、GCはルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをたどってマークします。この「たどる」プロセスにおいて、オブジェクトが他のオブジェクトへのポインタを含んでいる場合、GCはそのポインタをたどってさらにオブジェクトをマークします。

runtime·mallocgcFlagNoPointers

runtime·mallocgcは、Goランタイム内部で使用されるメモリ割り当て関数です。これは、GCによって管理されるヒープメモリを割り当てる際に使用されます。この関数は、割り当てるメモリのサイズ、アラインメント、そしてGCに関するフラグを受け取ります。

FlagNoPointersは、runtime·mallocgcに渡されるフラグの一つで、割り当てられるメモリ領域にポインタが含まれていないことをGCに伝えるために使用されます。このフラグが設定されている場合、GCはこのメモリ領域をポインタの有無についてスキャンしません。これは、GCのオーバーヘッドを削減するために使用される最適化手法の一つです。例えば、純粋なデータ構造(整数や浮動小数点数のみを含む配列など)を割り当てる際にこのフラグを使用することで、GCがその領域を不必要にスキャンするのを避けることができます。

しかし、このフラグを誤って使用すると、GCが実際にポインタを含むメモリ領域をスキャンしなくなり、その結果、到達可能なオブジェクトが誤って解放されたり、メモリリークが発生したりする可能性があります。

Goのハッシュマップ (map) の内部構造

Goのmap型は、内部的にはハッシュテーブルとして実装されています。ハッシュテーブルは、キーと値のペアを格納するために「バケット」と呼ばれる配列を使用します。各バケットは、複数のキーと値のペアを保持できる小さな構造体です。ハッシュ衝突が発生した場合(異なるキーが同じバケットにハッシュされる場合)、Goのマップはオーバーフローバケットやチェイニングなどのメカニズムを使用して衝突を解決します。

マップが成長すると、より多くのバケットが必要になり、既存のバケット配列が拡張されたり、新しいバケット配列にデータが再ハッシュされたりすることがあります。このプロセス中に、古いバケット配列が一時的に存在し続けることがあります。

イテレータとGC

Goのマップはイテレータ(for rangeループなど)をサポートしています。イテレータは、マップの要素を順に処理するために使用されます。イテレータが動作している間、マップの内部構造(特にバケット配列)は変更されないように保護される必要があります。マップのサイズ変更(成長)が発生した場合、イテレータは古いバケット配列を参照し続けることがあります。

GCの観点から見ると、イテレータが参照している古いバケット配列は、まだ「到達可能」なメモリ領域と見なされるべきです。なぜなら、イテレータがその配列内のポインタを介してキーや値にアクセスする可能性があるからです。もしGCがこれらの古いバケット配列をスキャンしない場合、それらが参照しているキーや値のオブジェクトがGCによって誤って解放されてしまう可能性があります。

技術的詳細

このコミットは、src/pkg/runtime/hashmap.cファイルに対する変更を取り消しています。具体的には、runtime·mallocgc関数の呼び出しからFlagNoPointersフラグを削除しています。

元のコミットでは、ハッシュマップのバケット配列を割り当てる際に、GCがその配列をスキャンしないようにFlagNoPointersフラグが追加されていました。これは、バケット配列自体はポインタの配列ですが、GCはhash_gciterというカスタムイテレータを通じてこれらのポインタを明示的に処理するという意図がありました。このアプローチは、GCの効率を向上させ、バグ6119を修正することを目的としていました。

しかし、このコミットが示すように、この変更は「Breaks the build. Old bucket arrays kept by iterators still need to be scanned.」という問題を引き起こしました。これは、FlagNoPointersが設定されたことで、GCがバケット配列をスキャンしなくなり、その結果、イテレータが保持している古いバケット配列内のポインタがGCによって追跡されなくなったことを意味します。イテレータがまだアクティブであるにもかかわらず、GCが古いバケット配列を到達不可能と判断し、その中のポインタが指すオブジェクトを解放してしまう可能性がありました。これは、メモリ破損やクラッシュにつながる重大なバグです。

このコミットは、この問題を解決するために、runtime·mallocgc呼び出しからFlagNoPointersフラグを削除し、GCがハッシュマップのバケット配列を通常通りスキャンするように戻しました。これにより、イテレータが参照する古いバケット配列もGCによって適切に処理され、メモリの安全性が確保されます。

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

src/pkg/runtime/hashmap.c ファイルにおいて、runtime·mallocgc 関数の呼び出しから FlagNoPointers フラグが削除されています。

具体的には、以下の行で変更が行われています。

--- a/src/pkg/runtime/hashmap.c
+++ b/src/pkg/runtime/hashmap.c
@@ -259,10 +259,7 @@ hash_init(MapType *t, Hmap *h, uint32 hint)
 		// done lazily later.
 		buckets = nil;
 	} else {
-		buckets = runtime·mallocgc(bucketsize << B, 0, FlagNoZero | FlagNoPointers);
-		// Note: the array really does have pointers, but we tell the gc about
-		// them explicitly via gciter below.  We use FlagNoPointers to prevent
-		// the gc from scanning the bucket array itself.  Fixes issue 6119.
+		buckets = runtime·mallocgc(bucketsize << B, 0, FlagNoZero);
 		for(i = 0; i < (uintptr)1 << B; i++) {
 			b = (Bucket*)(buckets + i * bucketsize);
 			clearbucket(b);
@@ -333,7 +330,7 @@ evacuate(MapType *t, Hmap *h, uintptr oldbucket)
 			if((hash & newbit) == 0) {
 				if(xi == BUCKETSIZE) {
 					if(checkgc) mstats.next_gc = mstats.heap_alloc;
-					newx = runtime·mallocgc(h->bucketsize, 0, FlagNoZero | FlagNoPointers);
+					newx = runtime·mallocgc(h->bucketsize, 0, FlagNoZero);
 					clearbucket(newx);
 					x->overflow = newx;
 					x = newx;
@@ -358,7 +355,7 @@ evacuate(MapType *t, Hmap *h, uintptr oldbucket)
 				} else {
 					if(yi == BUCKETSIZE) {
 						if(checkgc) mstats.next_gc = mstats.heap_alloc;
-						newy = runtime·mallocgc(h->bucketsize, 0, FlagNoZero | FlagNoPointers);
+						newy = runtime·mallocgc(h->bucketsize, 0, FlagNoZero);
 						clearbucket(newy);
 						y->overflow = newy;
 						y = newy;
@@ -454,7 +451,7 @@ hash_grow(MapType *t, Hmap *h)
 	old_buckets = h->buckets;
 	// NOTE: this could be a big malloc, but since we don't need zeroing it is probably fast.
 	if(checkgc) mstats.next_gc = mstats.heap_alloc;
-	new_buckets = runtime·mallocgc((uintptr)h->bucketsize << (h->B + 1), 0, FlagNoZero | FlagNoPointers);
+	new_buckets = runtime·mallocgc((uintptr)h->bucketsize << (h->B + 1), 0, FlagNoZero);
 	flags = (h->flags & ~(Iterator | OldIterator));
 	if((h->flags & Iterator) != 0) {
 		flags |= OldIterator;
@@ -618,7 +615,7 @@ hash_insert(MapType *t, Hmap *h, void *key, void *value)
 	hash = h->hash0;
 	t->key->alg->hash(&hash, t->key->size, key);
 	if(h->buckets == nil) {
-		h->buckets = runtime·mallocgc(h->bucketsize, 0, FlagNoZero | FlagNoPointers);
+		h->buckets = runtime·mallocgc(h->bucketsize, 0, FlagNoZero);
 		b = (Bucket*)(h->buckets);
 		clearbucket(b);
 	}
@@ -668,7 +665,7 @@ hash_insert(MapType *t, Hmap *h, void *key, void *value)
 	if(inserti == nil) {
 		// all current buckets are full, allocate a new one.
 		if(checkgc) mstats.next_gc = mstats.heap_alloc;
-		newb = runtime·mallocgc(h->bucketsize, 0, FlagNoZero | FlagNoPointers);
+		newb = runtime·mallocgc(h->bucketsize, 0, FlagNoZero);
 		clearbucket(newb);
 		b->overflow = newb;
 		inserti = newb->tophash;

コアとなるコードの解説

上記の差分は、runtime/hashmap.c 内の複数の箇所で runtime·mallocgc の呼び出しから FlagNoPointers フラグが削除されていることを示しています。

  • hash_init 関数内: ハッシュマップが初期化される際に、最初のバケット配列が割り当てられます。元のコミットでは、ここでFlagNoPointersが使用されていましたが、このコミットで削除されました。
  • evacuate 関数内: マップのサイズ変更(成長)やバケットの再配置中に、新しいバケットが割り当てられることがあります。ここでもFlagNoPointersが削除されました。
  • hash_grow 関数内: マップが成長し、新しいより大きなバケット配列が必要になったときに呼び出されます。新しいバケット配列の割り当て時にFlagNoPointersが削除されました。
  • hash_insert 関数内: 新しい要素がマップに挿入される際に、必要に応じて新しいバケットが割り当てられることがあります。ここでもFlagNoPointersが削除されました。

これらの変更は、ハッシュマップのバケット配列がGCによって通常通りスキャンされるように戻すことを意味します。これにより、イテレータが参照する古いバケット配列内のポインタもGCによって適切に追跡され、メモリの安全性が確保されます。

元のコミットのコメントにあった「Note: the array really does have pointers, but we tell the gc about them explicitly via gciter below. We use FlagNoPointers to prevent the gc from scanning the bucket array itself. Fixes issue 6119.」という記述は、FlagNoPointersを使用しても、hash_gciterというカスタムGCイテレータがポインタをGCに明示的に伝えることで、GCが正しく動作すると考えていたことを示しています。しかし、このコミットの取り消しは、このアプローチがイテレータが保持する古いバケット配列のケースで問題を引き起こしたことを明確に示しています。

このコミットは、GCの最適化は重要であるものの、メモリ安全性と正確性を犠牲にしてはならないというGoランタイム開発の原則を強調しています。

関連リンク

参考にした情報源リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事
  • Goのmap実装に関する技術文書やソースコード解析
  • runtime·mallocgc および FlagNoPointers に関するGoランタイムのソースコードコメント
  • Goのイテレータの動作に関する情報

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

このコミットは、Goランタイムにおけるハッシュマップのメモリ管理とガベージコレクション(GC)に関する以前の変更(CL 12840043 / 3b9f54db72a1)を取り消すものです。具体的には、ハッシュマップの内部構造(バケット配列)がGCによってスキャンされないようにする試みを元に戻し、その変更がビルドを破壊し、イテレータによって保持されている古いバケット配列がGCによって適切にスキャンされる必要があるという問題に対処しています。

コミット

commit 74e78df107f9e6642ecfc6808f542e2625f7cfb2
Author: Keith Randall <khr@golang.org>
Date:   Tue Aug 13 12:59:39 2013 -0700

    undo CL 12840043 / 3b9f54db72a1
    
    Breaks the build.  Old bucket arrays kept by iterators
    still need to be scanned.
    
    ««« original CL description
    runtime: tell GC not to scan internal hashmap structures.
    We'll do it ourselves via hash_gciter, thanks.
    Fixes bug 6119.
    
    R=golang-dev, dvyukov, cookieo9, rsc
    CC=golang-dev
    https://golang.org/cl/12840043
    »»»
    
    R=golang-dev
    CC=golang-dev
    https://golang.org/cl/12884043

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

https://github.com/golang/go/commit/74e78df107f9e6642ecfc6808f542e2625f7cfb2

元コミット内容

このコミットが取り消した元のコミット(CL 12840043 / 3b9f54db72a1)の目的は以下の通りです。

「runtime: tell GC not to scan internal hashmap structures. We'll do it ourselves via hash_gciter, thanks. Fixes bug 6119.」

これは、Goランタイムのハッシュマップ(map型)の内部構造(特にバケット配列)について、Goのガベージコレクタ(GC)が直接スキャンしないように指示し、代わりにhash_gciterというカスタムのGCイテレータを通じてこれらの構造をGCに認識させることで、バグ6119を修正しようとしたものです。この変更は、runtime·mallocgc関数を呼び出す際にFlagNoPointersフラグを追加することで実現されていました。

変更の背景

元のコミット(CL 12840043)は、GoのハッシュマップのGC処理を最適化し、特定のバグ(バグ6119)を修正することを目的としていました。バグ6119は、ハッシュマップのバケット配列がGCによって適切に処理されないことに関連する可能性があり、FlagNoPointersフラグを使用してGCの挙動を変更することで、この問題を解決しようとしました。

しかし、このコミット(CL 12884043)のコミットメッセージによると、元の変更は「Breaks the build. Old bucket arrays kept by iterators still need to be scanned.」という問題を引き起こしました。これは、ハッシュマップのイテレータが古いバケット配列を参照し続けている場合、GCがこれらの古い配列をスキャンしないと、参照されているオブジェクトが誤って解放されてしまう可能性があることを示唆しています。つまり、FlagNoPointersフラグによってGCがバケット配列をスキャンしなくなった結果、イテレータが保持する古いバケット配列内のポインタがGCによって追跡されなくなり、メモリ安全性や正確なGC処理が損なわれるという重大な問題が発生したため、元の変更を取り消す必要が生じました。

前提知識の解説

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

Goのガベージコレクションは、プログラムが動的に確保したメモリのうち、もはや到達不可能(参照されていない)なオブジェクトを自動的に解放する仕組みです。GoのGCは並行マーク&スイープ方式を採用しており、プログラムの実行と並行してGCが動作することで、アプリケーションの一時停止時間(ストップ・ザ・ワールド時間)を最小限に抑えることを目指しています。

GCは、プログラムが使用しているオブジェクトを「マーク」し、マークされなかったオブジェクトを「スイープ」(解放)します。このマークフェーズでは、GCはルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをたどってマークします。この「たどる」プロセスにおいて、オブジェクトが他のオブジェクトへのポインタを含んでいる場合、GCはそのポインタをたどってさらにオブジェクトをマークします。

runtime·mallocgcFlagNoPointers

runtime·mallocgcは、Goランタイム内部で使用されるメモリ割り当て関数です。これは、GCによって管理されるヒープメモリを割り当てる際に使用されます。この関数は、割り当てるメモリのサイズ、アラインメント、そしてGCに関するフラグを受け取ります。

FlagNoPointersは、runtime·mallocgcに渡されるフラグの一つで、割り当てられるメモリ領域にポインタが含まれていないことをGCに伝えるために使用されます。このフラグが設定されている場合、GCはこのメモリ領域をポインタの有無についてスキャンしません。これは、GCのオーバーヘッドを削減するために使用される最適化手法の一つです。例えば、純粋なデータ構造(整数や浮動小数点数のみを含む配列など)を割り当てる際にこのフラグを使用することで、GCがその領域を不必要にスキャンするのを避けることができます。

しかし、このフラグを誤って使用すると、GCが実際にポインタを含むメモリ領域をスキャンしなくなり、その結果、到達可能なオブジェクトが誤って解放されたり、メモリリークが発生したりする可能性があります。

Goのハッシュマップ (map) の内部構造

Goのmap型は、内部的にはハッシュテーブルとして実装されています。ハッシュテーブルは、キーと値のペアを格納するために「バケット」と呼ばれる配列を使用します。各バケットは、複数のキーと値のペアを保持できる小さな構造体です。ハッシュ衝突が発生した場合(異なるキーが同じバケットにハッシュされる場合)、Goのマップはオーバーフローバケットやチェイニングなどのメカニズムを使用して衝突を解決します。

マップが成長すると、より多くのバケットが必要になり、既存のバケット配列が拡張されたり、新しいバケット配列にデータが再ハッシュされたりすることがあります。このプロセス中に、古いバケット配列が一時的に存在し続けることがあります。

イテレータとGC

Goのマップはイテレータ(for rangeループなど)をサポートしています。イテレータは、マップの要素を順に処理するために使用されます。イテレータが動作している間、マップの内部構造(特にバケット配列)は変更されないように保護される必要があります。マップのサイズ変更(成長)が発生した場合、イテレータは古いバケット配列を参照し続けることがあります。

GCの観点から見ると、イテレータが参照している古いバケット配列は、まだ「到達可能」なメモリ領域と見なされるべきです。なぜなら、イテレータがその配列内のポインタを介してキーや値にアクセスする可能性があるからです。もしGCがこれらの古いバケット配列をスキャンしない場合、それらが参照しているキーや値のオブジェクトがGCによって誤って解放されてしまう可能性があります。

技術的詳細

このコミットは、src/pkg/runtime/hashmap.cファイルに対する変更を取り消しています。具体的には、runtime·mallocgc関数の呼び出しからFlagNoPointersフラグを削除しています。

元のコミットでは、ハッシュマップのバケット配列を割り当てる際に、GCがその配列をスキャンしないようにFlagNoPointersフラグが追加されていました。これは、バケット配列自体はポインタの配列ですが、GCはhash_gciterというカスタムイテレータを通じてこれらのポインタを明示的に処理するという意図がありました。このアプローチは、GCの効率を向上させ、バグ6119を修正することを目的としていました。

しかし、このコミットが示すように、この変更は「Breaks the build. Old bucket arrays kept by iterators still need to be scanned.」という問題を引き起こしました。これは、FlagNoPointersが設定されたことで、GCがバケット配列をスキャンしなくなり、その結果、イテレータが保持している古いバケット配列内のポインタがGCによって追跡されなくなったことを意味します。イテレータがまだアクティブであるにもかかわらず、GCが古いバケット配列を到達不可能と判断し、その中のポインタが指すオブジェクトを解放してしまう可能性がありました。これは、メモリ破損やクラッシュにつながる重大なバグです。

このコミットは、この問題を解決するために、runtime·mallocgc呼び出しからFlagNoPointersフラグを削除し、GCがハッシュマップのバケット配列を通常通りスキャンするように戻しました。これにより、イテレータが参照する古いバケット配列もGCによって適切に処理され、メモリの安全性が確保されます。

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

src/pkg/runtime/hashmap.c ファイルにおいて、runtime·mallocgc 関数の呼び出しから FlagNoPointers フラグが削除されています。

具体的には、以下の行で変更が行われています。

--- a/src/pkg/runtime/hashmap.c
+++ b/src/pkg/runtime/hashmap.c
@@ -259,10 +259,7 @@ hash_init(MapType *t, Hmap *h, uint32 hint)
 		// done lazily later.
 		buckets = nil;
 	} else {
-		buckets = runtime·mallocgc(bucketsize << B, 0, FlagNoZero | FlagNoPointers);
-		// Note: the array really does have pointers, but we tell the gc about
-		// them explicitly via gciter below.  We use FlagNoPointers to prevent
-		// the gc from scanning the bucket array itself.  Fixes issue 6119.
+		buckets = runtime·mallocgc(bucketsize << B, 0, FlagNoZero);
 		for(i = 0; i < (uintptr)1 << B; i++) {
 			b = (Bucket*)(buckets + i * bucketsize);
 			clearbucket(b);
@@ -333,7 +330,7 @@ evacuate(MapType *t, Hmap *h, uintptr oldbucket)
 			if((hash & newbit) == 0) {
 				if(xi == BUCKETSIZE) {
 					if(checkgc) mstats.next_gc = mstats.heap_alloc;
-					newx = runtime·mallocgc(h->bucketsize, 0, FlagNoZero | FlagNoPointers);
+					newx = runtime·mallocgc(h->bucketsize, 0, FlagNoZero);
 					clearbucket(newx);
 					x->overflow = newx;
 					x = newx;
@@ -358,7 +355,7 @@ evacuate(MapType *t, Hmap *h, uintptr oldbucket)
 				} else {
 					if(yi == BUCKETSIZE) {
 						if(checkgc) mstats.next_gc = mstats.heap_alloc;
-						newy = runtime·mallocgc(h->bucketsize, 0, FlagNoZero | FlagNoPointers);
+						newy = runtime·mallocgc(h->bucketsize, 0, FlagNoZero);
 						clearbucket(newy);
 						y->overflow = newy;
 						y = newy;
@@ -454,7 +451,7 @@ hash_grow(MapType *t, Hmap *h)
 	old_buckets = h->buckets;
 	// NOTE: this could be a big malloc, but since we don't need zeroing it is probably fast.
 	if(checkgc) mstats.next_gc = mstats.heap_alloc;
-	new_buckets = runtime·mallocgc((uintptr)h->bucketsize << (h->B + 1), 0, FlagNoZero | FlagNoPointers);
+	new_buckets = runtime·mallocgc((uintptr)h->bucketsize << (h->B + 1), 0, FlagNoZero);
 	flags = (h->flags & ~(Iterator | OldIterator));
 	if((h->flags & Iterator) != 0) {
 		flags |= OldIterator;
@@ -618,7 +615,7 @@ hash_insert(MapType *t, Hmap *h, void *key, void *value)
 	hash = h->hash0;
 	t->key->alg->hash(&hash, t->key->size, key);
 	if(h->buckets == nil) {
-		h->buckets = runtime·mallocgc(h->bucketsize, 0, FlagNoZero | FlagNoPointers);
+		h->buckets = runtime·mallocgc(h->bucketsize, 0, FlagNoZero);
 		b = (Bucket*)(h->buckets);
 		clearbucket(b);
 	}
@@ -668,7 +665,7 @@ hash_insert(MapType *t, Hmap *h, void *key, void *value)
 	if(inserti == nil) {
 		// all current buckets are full, allocate a new one.
 		if(checkgc) mstats.next_gc = mstats.heap_alloc;
-		newb = runtime·mallocgc(h->bucketsize, 0, FlagNoZero | FlagNoPointers);
+		newb = runtime·mallocgc(h->bucketsize, 0, FlagNoZero);
 		clearbucket(newb);
 		b->overflow = newb;
 		inserti = newb->tophash;

コアとなるコードの解説

上記の差分は、runtime/hashmap.c 内の複数の箇所で runtime·mallocgc の呼び出しから FlagNoPointers フラグが削除されていることを示しています。

  • hash_init 関数内: ハッシュマップが初期化される際に、最初のバケット配列が割り当てられます。元のコミットでは、ここでFlagNoPointersが使用されていましたが、このコミットで削除されました。
  • evacuate 関数内: マップのサイズ変更(成長)やバケットの再配置中に、新しいバケットが割り当てられることがあります。ここでもFlagNoPointersが削除されました。
  • hash_grow 関数内: マップが成長し、新しいより大きなバケット配列が必要になったときに呼び出されます。新しいバケット配列の割り当て時にFlagNoPointersが削除されました。
  • hash_insert 関数内: 新しい要素がマップに挿入される際に、必要に応じて新しいバケットが割り当てられることがあります。ここでもFlagNoPointersが削除されました。

これらの変更は、ハッシュマップのバケット配列がGCによって通常通りスキャンされるように戻すことを意味します。これにより、イテレータが参照する古いバケット配列内のポインタもGCによって適切に追跡され、メモリの安全性が確保されます。

元のコミットのコメントにあった「Note: the array really does have pointers, but we tell the gc about them explicitly via gciter below. We use FlagNoPointers to prevent the gc from scanning the bucket array itself. Fixes issue 6119.」という記述は、FlagNoPointersを使用しても、hash_gciterというカスタムGCイテレータがポインタをGCに明示的に伝えることで、GCが正しく動作すると考えていたことを示しています。しかし、このコミットの取り消しは、このアプローチがイテレータが保持する古いバケット配列のケースで問題を引き起こしたことを明確に示しています。

このコミットは、GCの最適化は重要であるものの、メモリ安全性と正確性を犠牲にしてはならないというGoランタイム開発の原則を強調しています。

関連リンク

参考にした情報源リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事
  • Goのmap実装に関する技術文書やソースコード解析
  • runtime·mallocgc および FlagNoPointers に関するGoランタイムのソースコードコメント
  • Goのイテレータの動作に関する情報
  • FlagNoPointersに関するWeb検索結果: FlagNoPointersはGoランタイム内部のフラグであり、特定の型がポインタを含まないことをGCに伝えるために使用されます。これによりGCはスキャンを最適化できますが、Go開発者が直接操作するものではありません。