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

[インデックス 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言語のマップは、キーと値を効率的に格納するために内部的にハッシュテーブルを使用しています。しかし、キーや値のサイズが大きくなると、ハッシュテーブルのエントリに直接データを格納することが非効率になったり、メモリレイアウトに問題が生じたりする可能性があります。

このコミットの背景には、以下の課題がありました。

  1. 大きな値の効率的な格納: 従来のマップ実装では、キーと値の合計サイズが255バイトを超える場合に、そのデータを直接ハッシュテーブルのエントリに格納することが困難でした。これにより、メモリの断片化やコピーコストの増加といった問題が発生する可能性がありました。
  2. ガベージコレクションとの連携: Goのガベージコレクタは、ポインタを追跡して到達可能なオブジェクトを特定します。マップの内部構造が複雑になると、ガベージコレクタが正確にポインタを識別し、不要なメモリを解放することが難しくなる場合があります。特に、reflectパッケージを介してマップが操作される場合、ガベージコレクタがマップの内部構造を正しく理解し、メモリリークを防ぐことが重要でした。
  3. Issue #3573の修正: このコミットは、GoのIssue #3573「map[string][1000000]byte causes panic: runtime error: makeslice: len out of range」を修正することを目的としています。この問題は、非常に大きな配列を値とするマップを作成しようとすると、ランタイムパニックが発生するというものでした。これは、マップが大きな値を効率的に扱えないことに起因していました。

これらの課題に対処するため、Goランタイムのマップ実装において、大きなキーや値を間接的に参照するメカニズムを導入し、メモリ管理とガベージコレクションの挙動を改善する必要がありました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の内部実装に関する知識が役立ちます。

  1. Goのマップ(map)の内部構造:

    • Goのマップは、内部的にハッシュテーブルとして実装されています。
    • ハッシュテーブルは、キーのハッシュ値に基づいてデータを格納するバケット(またはサブテーブル)の集合で構成されます。
    • 各エントリは、ハッシュ値、キー、値のデータを保持します。
    • キーと値のデータは、通常、エントリ内に直接インラインで格納されます。
    • マップのサイズが大きくなると、ハッシュテーブルは動的にリサイズ(成長)されます。
  2. runtimeパッケージ:

    • runtimeパッケージは、Goプログラムの実行環境を管理するGo言語のコアライブラリです。
    • ガベージコレクション、スケジューリング、メモリ管理、マップやチャネルなどの組み込み型の低レベルな実装が含まれています。
    • C言語で書かれた部分(src/pkg/runtime/hashmap.cなど)とGo言語で書かれた部分があります。
  3. reflectパッケージ:

    • reflectパッケージは、Goプログラムが実行時に自身の構造を検査し、操作するための機能を提供します。
    • これにより、型情報、フィールド、メソッドなどを動的に取得したり、値を設定したりすることができます。
    • マップに対してもreflectパッケージを通じて操作を行うことができ、その際にはマップの内部構造と密接に連携します。
  4. ポインタと間接参照:

    • Goでは、変数はその値自体を保持するか、または値が格納されているメモリ上のアドレス(ポインタ)を保持することができます。
    • 大きなデータ構造を扱う場合、データ全体をコピーする代わりに、そのデータへのポインタを渡すことで、メモリコピーのオーバーヘッドを削減し、効率を向上させることができます。これを「間接参照」と呼びます。
  5. ガベージコレクション(GC):

    • Goは自動メモリ管理(ガベージコレクション)を採用しています。
    • GCは、プログラムがもはや参照しないメモリ領域を自動的に解放し、メモリリークを防ぎます。
    • GCが正しく機能するためには、プログラムが使用しているすべてのポインタを正確に追跡できる必要があります。
  6. 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);

    // ...
}

このロジックにより、キーや値のサイズに応じて IndirectKeyIndirectVal フラグが設定され、ksizevsize がポインタのサイズに調整されます。これにより、マップのエントリには常に最大255バイトのデータ(またはポインタ)が格納されるようになります。

4. キーと値へのアクセス関数の導入

キーや値が間接的に格納されている場合でも、統一された方法でアクセスできるように、新しいヘルパー関数が導入されました。

  • static void** hash_valptr(Hmap *h, void *p): 値へのポインタを返します。値が間接的に格納されている場合は、そのポインタをデリファレンスします。
  • static void** hash_keyptr(Hmap *h, void *p): キーへのポインタを返します。キーが間接的に格納されている場合は、そのポインタをデリファレンスします。

これらの関数は、runtime·mapaccessruntime·mapassignruntime·mapiter1runtime·mapiterkeyreflect·mapiterkeyruntime·mapiter2 など、マップのキーや値にアクセスするすべての場所で使用されるようになりました。

5. メモリ解放ロジックの変更

hash_growclean_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のマップは、キーや値のサイズに関わらず、より効率的かつ安全に動作するようになりました。特に、大きなデータ構造をマップのキーや値として使用する際のパフォーマンスと安定性が向上しました。

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

このコミットにおけるコアとなるコードの変更箇所は、主に以下のファイルと関数に集中しています。

  1. 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パッケージとの連携のためのフラグ操作。
  2. test/bigmap.go:

    • 様々なサイズのキーと値を持つマップの挙動を検証するための新しいテストケースの追加。

これらの変更は、Goのマップが内部的にどのようにメモリを管理し、キーと値を格納するかという、ランタイムの根幹に関わる部分に影響を与えています。

コアとなるコードの解説

Hmap 構造体の flag フィールドと間接参照の決定

最も重要な変更は、Hmap 構造体における flag フィールドの導入です。これにより、マップがキーや値をどのように格納しているか(直接か間接か)を柔軟に表現できるようになりました。

runtime·makemap_c 関数内で、新しいマップが作成される際に、キーと値のサイズに基づいて IndirectKeyIndirectVal フラグが設定されます。

    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*); // 値のサイズをポインタのサイズに設定
        }
    }

このロジックは、以下の優先順位で間接参照を決定します。

  1. キーが大きすぎる場合: ksize > MaxData - sizeof(void*) の条件は、キーのサイズが、マップエントリに直接格納できる最大データサイズからポインタのサイズを引いた値よりも大きい場合に真となります。これは、キーを直接格納すると MaxData を超えてしまうため、キーを間接参照にする必要があることを意味します。この場合、IndirectKey フラグが設定され、ksize はポインタのサイズに設定されます。
  2. 値が大きすぎる場合: 上記のキーのチェックの後、残りの MaxData - ksize の領域に値を格納できるかをチェックします。vsize > MaxData - ksize の条件が真の場合、値を間接参照にする必要があります。この場合、IndirectVal フラグが設定され、vsize はポインタのサイズに設定されます。

この仕組みにより、マップのエントリは常に MaxData(255バイト)以下のサイズに収まり、大きなキーや値はヒープ上に別途割り当てられ、マップエントリからはそのポインタが参照されるようになります。これにより、マップの内部構造がコンパクトに保たれ、メモリ効率が向上します。

hash_valptrhash_keyptr による統一されたアクセス

キーや値が直接格納されているか、間接的に格納されているかにかかわらず、マップの操作関数が統一された方法でキーや値にアクセスできるように、hash_valptrhash_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;
}

これらの関数は、マップエントリ内のデータブロックの先頭ポインタ pHmap 構造体 h を受け取ります。IndirectValIndirectKey フラグが設定されている場合は、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言語の堅牢性がさらに向上しました。

関連リンク

参考にした情報源リンク

  • 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.