[インデックス 13169] ファイルの概要
このコミットは、Go言語のランタイムにおけるマップ(map
)の実装において、大きなサイズのキーや値を効率的に扱うための改善と、それに関連するテストの追加を目的としています。具体的には、マップのエントリが一定サイズ(255バイト)を超える場合に、キーや値を直接マップのデータ構造内に格納するのではなく、ポインタを介して間接的に参照するように変更し、メモリ管理とパフォーマンスを最適化しています。また、reflect
パッケージがマップを操作する際の挙動も考慮し、ガベージコレクションの安全性も確保しています。
コミット
commit bf18d57d4a186302ed7a3b07d60cd6facda08a71
Author: Russ Cox <rsc@golang.org>
Date: Thu May 24 22:41:07 2012 -0400
runtime: handle and test large map values
This is from CL 5451105 but was dropped from that CL.
See also CL 6137051.
The only change compared to 5451105 is to check for
h != nil in reflect·mapiterinit; allowing use of nil maps
must have happened after that original CL.
Fixes #3573.
R=golang-dev, dave, r
CC=golang-dev
https://golang.org/cl/6215078
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bf18d57d4a186302ed7a3b07d60cd6facda08a71
元コミット内容
このコミットは、元々CL 5451105の一部として提案されましたが、そのCLからは除外された変更です。その後、CL 6137051に関連する形で、改めてこのコミットとして取り込まれました。CL 5451105との唯一の変更点は、reflect·mapiterinit
関数内でh != nil
のチェックが追加されたことです。これは、元のCLが作成された後にnilマップの使用が許可されたことによる調整です。この変更は、Goのマップが大きな値を扱う際の挙動を改善し、Issue #3573を修正することを目的としています。
変更の背景
Go言語のマップは、キーと値を効率的に格納するために内部的にハッシュテーブルを使用しています。しかし、キーや値のサイズが大きくなると、ハッシュテーブルのエントリに直接データを格納することが非効率になったり、メモリレイアウトに問題が生じたりする可能性があります。
このコミットの背景には、以下の課題がありました。
- 大きな値の効率的な格納: 従来のマップ実装では、キーと値の合計サイズが255バイトを超える場合に、そのデータを直接ハッシュテーブルのエントリに格納することが困難でした。これにより、メモリの断片化やコピーコストの増加といった問題が発生する可能性がありました。
- ガベージコレクションとの連携: Goのガベージコレクタは、ポインタを追跡して到達可能なオブジェクトを特定します。マップの内部構造が複雑になると、ガベージコレクタが正確にポインタを識別し、不要なメモリを解放することが難しくなる場合があります。特に、
reflect
パッケージを介してマップが操作される場合、ガベージコレクタがマップの内部構造を正しく理解し、メモリリークを防ぐことが重要でした。 - Issue #3573の修正: このコミットは、GoのIssue #3573「
map[string][1000000]byte
causespanic: runtime error: makeslice: len out of range
」を修正することを目的としています。この問題は、非常に大きな配列を値とするマップを作成しようとすると、ランタイムパニックが発生するというものでした。これは、マップが大きな値を効率的に扱えないことに起因していました。
これらの課題に対処するため、Goランタイムのマップ実装において、大きなキーや値を間接的に参照するメカニズムを導入し、メモリ管理とガベージコレクションの挙動を改善する必要がありました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の内部実装に関する知識が役立ちます。
-
Goのマップ(
map
)の内部構造:- Goのマップは、内部的にハッシュテーブルとして実装されています。
- ハッシュテーブルは、キーのハッシュ値に基づいてデータを格納するバケット(またはサブテーブル)の集合で構成されます。
- 各エントリは、ハッシュ値、キー、値のデータを保持します。
- キーと値のデータは、通常、エントリ内に直接インラインで格納されます。
- マップのサイズが大きくなると、ハッシュテーブルは動的にリサイズ(成長)されます。
-
runtime
パッケージ:runtime
パッケージは、Goプログラムの実行環境を管理するGo言語のコアライブラリです。- ガベージコレクション、スケジューリング、メモリ管理、マップやチャネルなどの組み込み型の低レベルな実装が含まれています。
- C言語で書かれた部分(
src/pkg/runtime/hashmap.c
など)とGo言語で書かれた部分があります。
-
reflect
パッケージ:reflect
パッケージは、Goプログラムが実行時に自身の構造を検査し、操作するための機能を提供します。- これにより、型情報、フィールド、メソッドなどを動的に取得したり、値を設定したりすることができます。
- マップに対しても
reflect
パッケージを通じて操作を行うことができ、その際にはマップの内部構造と密接に連携します。
-
ポインタと間接参照:
- Goでは、変数はその値自体を保持するか、または値が格納されているメモリ上のアドレス(ポインタ)を保持することができます。
- 大きなデータ構造を扱う場合、データ全体をコピーする代わりに、そのデータへのポインタを渡すことで、メモリコピーのオーバーヘッドを削減し、効率を向上させることができます。これを「間接参照」と呼びます。
-
ガベージコレクション(GC):
- Goは自動メモリ管理(ガベージコレクション)を採用しています。
- GCは、プログラムがもはや参照しないメモリ領域を自動的に解放し、メモリリークを防ぎます。
- GCが正しく機能するためには、プログラムが使用しているすべてのポインタを正確に追跡できる必要があります。
-
CL (Change List):
- Goプロジェクトでは、コードの変更は「Change List (CL)」として提案され、レビューを経てコミットされます。
- コミットメッセージに記載されているCL番号は、Goのコードレビューシステム(Gerrit)上の特定の変更セットを指します。
これらの知識を前提として、このコミットがGoのマップの内部実装にどのように影響を与え、大きなキーや値を効率的に、かつガベージコレクションと安全に連携して扱うように改善したかを詳細に見ていきます。
技術的詳細
このコミットの主要な技術的変更点は、Goのマップがキーと値を格納する方法を、そのサイズに基づいて動的に切り替えるメカニズムを導入したことです。具体的には、キーと値の合計サイズがMaxData
(255バイト)を超える場合に、それらを直接マップのエントリに格納するのではなく、ポインタを介して間接的に参照するように変更されました。
以下に、変更された主要な構造体、定数、および関数の詳細を説明します。
1. Hmap
構造体の変更
src/pkg/runtime/hashmap.c
に定義されている Hmap
構造体は、マップの内部状態を管理します。このコミットでは、indirectval
フィールドが削除され、代わりに flag
フィールドが導入されました。
struct Hmap { /* a hash table; initialize with hash_init() */
uint32 count; /* elements in table - must be first */
uint8 datasize; /* amount of data to store in entry */
// uint8 max_power; /* max power of 2 to create sub-tables */ // 削除
// uint8 indirectval; /* storing pointers to values */ // 削除
uint8 flag; // 新規追加
uint8 valoff; /* offset of value in key+value data block */
int32 changes; /* inc'ed whenever a subtable is created/grown */
uintptr hash0; /* hash seed */
struct hash_subtable *st; /* first-level table */
};
flag
フィールドは、以下のビットフラグを保持し、マップの挙動を制御します。
#define IndirectVal (1<<0)
: 値がポインタを介して間接的に格納されていることを示します。#define IndirectKey (1<<1)
: キーがポインタを介して間接的に格納されていることを示します。#define CanFreeTable (1<<2)
: サブテーブルを解放しても安全であることを示します。#define CanFreeKey (1<<3)
: キーへのポインタを解放しても安全であることを示します。
2. 新しい定数
#define MaxData 255
: マップのエントリに直接格納できるキーと値の合計データの最大サイズを定義します。これを超える場合は、間接参照が使用されます。#define HASH_MAX_POWER 12
: ハッシュサブテーブルの最大パワー(サイズ)を定義します。
3. runtime·makemap_c
関数の変更
runtime·makemap_c
は、新しいマップを作成する際に呼び出される関数です。この関数は、キーと値の型に基づいて、それらを直接格納するか、間接的に格納するかを決定するロジックが追加されました。
Hmap*
runtime·makemap_c(MapType *typ, int64 hint)
{
Hmap *h;
Type *key, *val;
uintptr ksize, vsize;
// ... (キーと値の型のチェック)
h = runtime·mal(sizeof(*h));
h->flag |= CanFreeTable; /* until reflect gets involved, free is okay */
ksize = runtime·rnd(key->size, sizeof(void*));
vsize = runtime·rnd(val->size, sizeof(void*));
if(ksize > MaxData || vsize > MaxData || ksize+vsize > MaxData) {
// キー、値、またはその両方が大きすぎる場合
if(ksize > MaxData - sizeof(void*)) {
// キーが大きすぎる場合、キーを間接参照にする
h->flag |= IndirectKey;
h->flag |= CanFreeKey; /* until reflect gets involved, free is okay */
ksize = sizeof(void*); // キーのサイズをポインタのサイズに設定
}
if(vsize > MaxData - ksize) {
// 値が大きすぎる場合、値を間接参照にする
h->flag |= IndirectVal;
vsize = sizeof(void*); // 値のサイズをポインタのサイズに設定
}
}
h->valoff = ksize;
hash_init(h, ksize+vsize, hint);
// ...
}
このロジックにより、キーや値のサイズに応じて IndirectKey
や IndirectVal
フラグが設定され、ksize
や vsize
がポインタのサイズに調整されます。これにより、マップのエントリには常に最大255バイトのデータ(またはポインタ)が格納されるようになります。
4. キーと値へのアクセス関数の導入
キーや値が間接的に格納されている場合でも、統一された方法でアクセスできるように、新しいヘルパー関数が導入されました。
static void** hash_valptr(Hmap *h, void *p)
: 値へのポインタを返します。値が間接的に格納されている場合は、そのポインタをデリファレンスします。static void** hash_keyptr(Hmap *h, void *p)
: キーへのポインタを返します。キーが間接的に格納されている場合は、そのポインタをデリファレンスします。
これらの関数は、runtime·mapaccess
、runtime·mapassign
、runtime·mapiter1
、runtime·mapiterkey
、reflect·mapiterkey
、runtime·mapiter2
など、マップのキーや値にアクセスするすべての場所で使用されるようになりました。
5. メモリ解放ロジックの変更
hash_grow
や clean_st
といったメモリ解放に関連する関数では、CanFreeTable
フラグがチェックされるようになりました。これにより、reflect
パッケージがマップの内部構造へのポインタを保持している可能性がある場合に、誤ってメモリを解放してしまうことを防ぎます。
また、hash_remove
関数では、IndirectKey
フラグが設定されている場合に、キーのメモリを解放するロジックが追加されました。ただし、reflect
パッケージがキーへのポインタを保持している可能性がある場合は、ガベージコレクタに任せるために解放をスキップします。
6. reflect·mapiterinit
の変更
reflect·mapiterinit
関数は、reflect
パッケージがマップのイテレータを初期化する際に呼び出されます。この関数では、マップのキーがポインタを介して間接的に格納されている場合に、CanFreeKey
フラグをクリアするロジックが追加されました。これは、reflect
パッケージがキーデータへのポインタを保持する可能性があるため、ランタイムがそのメモリを早期に解放しないようにするためです。
void
reflect·mapiterinit(MapType *t, Hmap *h, struct hash_iter *it)
{
uint8 flag;
if(h != nil && t->key->size > sizeof(void*)) {
// reflect·mapiterkey returns pointers to key data,
// and reflect holds them, so we cannot free key data
// eagerly anymore. Updating h->flag now is racy,
// but it's okay because this is the only possible store
// after creation.
flag = h->flag;
if(flag & IndirectKey)
flag &= ~CanFreeKey;
else
flag &= ~CanFreeTable;
h->flag = flag;
}
it = runtime·mal(sizeof *it);
FLUSH(&it);
runtime·mapiterinit(t, h, it);
}
7. テストの追加 (test/bigmap.go
)
test/bigmap.go
には、様々なサイズのキーと値を持つマップの挙動を検証するための新しいテストケースが追加されました。これにより、キーや値が小さい場合、キーが大きい場合、値が大きい場合、キーと値の両方が大きい場合など、様々なシナリオでマップが正しく機能することを確認できます。
これらの変更により、Goのマップは、キーや値のサイズに関わらず、より効率的かつ安全に動作するようになりました。特に、大きなデータ構造をマップのキーや値として使用する際のパフォーマンスと安定性が向上しました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルと関数に集中しています。
-
src/pkg/runtime/hashmap.c
:Hmap
構造体の定義変更(indirectval
からflag
への変更、新しいフラグの導入)。MaxData
定数の導入。runtime·makemap_c
関数における、キーと値のサイズに基づく間接参照の決定ロジック。hash_valptr
およびhash_keyptr
ヘルパー関数の導入。- マップの操作(
hash_lookup
,hash_remove
,hash_insert_internal
,runtime·mapaccess
,runtime·mapassign
,runtime·mapiter1
,runtime·mapiterkey
,runtime·mapiter2
)における、新しいアクセス関数の使用。 - メモリ解放(
hash_grow
,clean_st
)におけるCanFreeTable
フラグのチェック。 reflect·mapiterinit
関数における、reflect
パッケージとの連携のためのフラグ操作。
-
test/bigmap.go
:- 様々なサイズのキーと値を持つマップの挙動を検証するための新しいテストケースの追加。
これらの変更は、Goのマップが内部的にどのようにメモリを管理し、キーと値を格納するかという、ランタイムの根幹に関わる部分に影響を与えています。
コアとなるコードの解説
Hmap
構造体の flag
フィールドと間接参照の決定
最も重要な変更は、Hmap
構造体における flag
フィールドの導入です。これにより、マップがキーや値をどのように格納しているか(直接か間接か)を柔軟に表現できるようになりました。
runtime·makemap_c
関数内で、新しいマップが作成される際に、キーと値のサイズに基づいて IndirectKey
と IndirectVal
フラグが設定されます。
ksize = runtime·rnd(key->size, sizeof(void*));
vsize = runtime·rnd(val->size, sizeof(void*));
if(ksize > MaxData || vsize > MaxData || ksize+vsize > MaxData) {
// キー、値、またはその両方が大きすぎる場合
if(ksize > MaxData - sizeof(void*)) {
// キーが大きすぎる場合、キーを間接参照にする
h->flag |= IndirectKey;
h->flag |= CanFreeKey;
ksize = sizeof(void*); // キーのサイズをポインタのサイズに設定
}
if(vsize > MaxData - ksize) {
// 値が大きすぎる場合、値を間接参照にする
h->flag |= IndirectVal;
vsize = sizeof(void*); // 値のサイズをポインタのサイズに設定
}
}
このロジックは、以下の優先順位で間接参照を決定します。
- キーが大きすぎる場合:
ksize > MaxData - sizeof(void*)
の条件は、キーのサイズが、マップエントリに直接格納できる最大データサイズからポインタのサイズを引いた値よりも大きい場合に真となります。これは、キーを直接格納するとMaxData
を超えてしまうため、キーを間接参照にする必要があることを意味します。この場合、IndirectKey
フラグが設定され、ksize
はポインタのサイズに設定されます。 - 値が大きすぎる場合: 上記のキーのチェックの後、残りの
MaxData - ksize
の領域に値を格納できるかをチェックします。vsize > MaxData - ksize
の条件が真の場合、値を間接参照にする必要があります。この場合、IndirectVal
フラグが設定され、vsize
はポインタのサイズに設定されます。
この仕組みにより、マップのエントリは常に MaxData
(255バイト)以下のサイズに収まり、大きなキーや値はヒープ上に別途割り当てられ、マップエントリからはそのポインタが参照されるようになります。これにより、マップの内部構造がコンパクトに保たれ、メモリ効率が向上します。
hash_valptr
と hash_keyptr
による統一されたアクセス
キーや値が直接格納されているか、間接的に格納されているかにかかわらず、マップの操作関数が統一された方法でキーや値にアクセスできるように、hash_valptr
と hash_keyptr
というヘルパー関数が導入されました。
static void**
hash_valptr(Hmap *h, void *p)
{
p = (byte*)p + h->valoff; // 値のオフセットに移動
if(h->flag & IndirectVal) // IndirectVal フラグが立っている場合
p = *(void**)p; // ポインタをデリファレンス
return p;
}
static void**
hash_keyptr(Hmap *h, void *p)
{
if(h->flag & IndirectKey) // IndirectKey フラグが立っている場合
p = *(void**)p; // ポインタをデリファレンス
return p;
}
これらの関数は、マップエントリ内のデータブロックの先頭ポインタ p
と Hmap
構造体 h
を受け取ります。IndirectVal
や IndirectKey
フラグが設定されている場合は、p
が指すポインタをデリファレンスして実際のキーや値のデータへのポインタを返します。これにより、マップの他の関数は、キーや値がどのように格納されているかを意識することなく、これらのヘルパー関数を呼び出すだけで正しいデータへのポインタを取得できるようになりました。
reflect·mapiterinit
における CanFreeKey
/ CanFreeTable
の操作
reflect
パッケージは、Goの型システムを動的に操作するための強力な機能を提供します。マップに対してもreflect
パッケージを通じてイテレーションを行うことができます。この際、reflect
パッケージはマップの内部データへのポインタを保持する可能性があります。
reflect·mapiterinit
関数は、reflect
パッケージがマップのイテレータを初期化する際に呼び出されます。このコミットでは、この関数内で CanFreeKey
または CanFreeTable
フラグをクリアするロジックが追加されました。
if(h != nil && t->key->size > sizeof(void*)) {
// reflect·mapiterkey returns pointers to key data,
// and reflect holds them, so we cannot free key data
// eagerly anymore. Updating h->flag now is racy,
// but it's okay because this is the only possible store
// after creation.
flag = h->flag;
if(flag & IndirectKey)
flag &= ~CanFreeKey; // キーが間接参照の場合、キーの解放を禁止
else
flag &= ~CanFreeTable; // それ以外の場合、テーブルの解放を禁止
h->flag = flag;
}
このロジックは、reflect
パッケージがキーデータへのポインタを保持している可能性があるため、ランタイムがそのメモリを早期に解放しないようにするためのものです。もしreflect
がポインタを保持している間にランタイムがメモリを解放してしまうと、Use-After-Freeのような深刻なバグにつながる可能性があります。この変更により、ガベージコレクタがこれらのポインタを正しく追跡し、メモリの安全性を確保できるようになります。
これらのコアとなる変更により、Goのマップは、様々なサイズのキーと値を効率的かつ安全に扱うことができるようになり、Go言語の堅牢性がさらに向上しました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/bf18d57d4a186302ed7a3b07d60cd6facda08a71
- Go Issue #3573: https://github.com/golang/go/issues/3573
- Go Change List 6215078: https://golang.org/cl/6215078
- Go Change List 5451105 (関連): https://golang.org/cl/5451105
- Go Change List 6137051 (関連): https://golang.org/cl/6137051
参考にした情報源リンク
- Go言語のソースコード (
src/pkg/runtime/hashmap.c
,test/bigmap.go
) - Go Issue Tracker (Issue #3573)
- Go Code Review (Gerrit) (CL 6215078, CL 5451105, CL 6137051)
- Go言語のマップに関する一般的なドキュメントや解説記事 (Goのマップの内部実装に関する知識)
- Go言語の
reflect
パッケージに関するドキュメント (Goのreflect
パッケージの動作に関する知識) - ガベージコレクションに関する一般的な情報 (Goのガベージコレクションの仕組みに関する知識)
- C言語のポインタとメモリ管理に関する一般的な知識I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. The output is provided directly to standard output as requested.