ホームページ  >  記事  >  バックエンド開発  >  Python仮想マシンにおける辞書の実装原理は何ですか

Python仮想マシンにおける辞書の実装原理は何ですか

王林
王林転載
2023-05-19 20:19:04809ブラウズ

辞書データ構造分析

/* The ma_values pointer is NULL for a combined table
 * or points to an array of PyObject* for a split table
 */
typedef struct {
    PyObject_HEAD
    Py_ssize_t ma_used;
    PyDictKeysObject *ma_keys;
    PyObject **ma_values;
} PyDictObject;
 
struct _dictkeysobject {
    Py_ssize_t dk_refcnt;
    Py_ssize_t dk_size;
    dict_lookup_func dk_lookup;
    Py_ssize_t dk_usable;
    PyDictKeyEntry dk_entries[1];
};
 
typedef struct {
    /* Cached hash code of me_key. */
    Py_hash_t me_hash;
    PyObject *me_key;
    PyObject *me_value; /* This field is only meaningful for combined tables */
} PyDictKeyEntry;

Python仮想マシンにおける辞書の実装原理は何ですか

上記の各フィールドの意味は次のとおりです:

  • ob_refcnt、オブジェクトの参照カウント。

  • ob_type、オブジェクトのデータ型。

  • ma_used、現在のハッシュ テーブル内のデータの数。

  • ma_keys は、キーと値のペアを保持する配列を指します。

  • ma_values、これは値の配列を指しますが、_dictkeysobject の PyDictKeyEntry 配列内のオブジェクトも値を格納できるため、この値は必ずしも cpython の特定の実装で使用されるわけではありません。この値は、すべてのキーが文字列である場合にのみ使用できます。この記事では、PyDictKeyEntry の値は主に辞書の実装について説明するために使用されるため、この変数は無視してかまいません。

  • dk_refcnt, これは参照カウントを表すためにも使用されます。これは辞書ビューに関連しています。原理は参照カウントと似ているため、ここでは無視します。

  • dk_size、これはハッシュ テーブルのサイズを表し、2n である必要があります。この場合、モジュラー演算はビット単位の AND 演算に変えることができます。

  • dk_lookup、これはハッシュ テーブルのルックアップ関数を表し、関数ポインターです。

  • dk_usable は、現在の配列で使用できるキーと値のペアの数を示します。

  • dk_entries、ハッシュ テーブル。キーと値のペアが実際に保存されます。

  • #ハッシュ テーブル全体のレイアウトは次のようになります。

#新しい辞書オブジェクトの作成Python仮想マシンにおける辞書の実装原理は何ですか

This この関数はまだ比較的単純で、最初にメモリ空間を適用し、次にいくつかの初期化操作を実行し、キーと値のペアを保存するためのハッシュ テーブルを適用します。

static PyObject *
dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyObject *self;
    PyDictObject *d;
 
    assert(type != NULL && type->tp_alloc != NULL);
    // 申请内存空间
    self = type->tp_alloc(type, 0);
    if (self == NULL)
        return NULL;
    d = (PyDictObject *)self;
 
    /* The object has been implicitly tracked by tp_alloc */
    if (type == &PyDict_Type)
        _PyObject_GC_UNTRACK(d);
    // 因为还没有增加数据 因此哈希表当中 ma_used = 0
    d->ma_used = 0;
    // 申请保存键值对的数组  PyDict_MINSIZE_COMBINED 是一个宏定义 值为 8 表示哈希表数组的最小长度
    d->ma_keys = new_keys_object(PyDict_MINSIZE_COMBINED);
    // 如果申请失败返回 NULL
    if (d->ma_keys == NULL) {
        Py_DECREF(self);
        return NULL;
    }
    return self;
}
 
// new_keys_object 函数如下所示
static PyDictKeysObject *new_keys_object(Py_ssize_t size)
{
    PyDictKeysObject *dk;
    Py_ssize_t i;
    PyDictKeyEntry *ep0;
 
    assert(size >= PyDict_MINSIZE_SPLIT);
    assert(IS_POWER_OF_2(size));
    // 这里是申请内存的位置真正申请内存空间的大小为 PyDictKeysObject 的大小加上 size-1 个PyDictKeyEntry的大小
    // 这里你可能会有一位为啥不是 size 个 PyDictKeyEntry 的大小 因为在结构体 PyDictKeysObject 当中已经申请了一个 PyDictKeyEntry 对象了
    dk = PyMem_MALLOC(sizeof(PyDictKeysObject) +
                      sizeof(PyDictKeyEntry) * (size-1));
    if (dk == NULL) {
        PyErr_NoMemory();
        return NULL;
    }
    // 下面主要是一些初始化的操作 dk_refcnt 设置成 1 因为目前只有一个字典对象使用 这个 PyDictKeysObject 对象
    DK_DEBUG_INCREF dk->dk_refcnt = 1;
    dk->dk_size = size; // 哈希表的大小
    // 下面这行代码主要是表示哈希表当中目前还能存储多少个键值对 在 cpython 的实现当中允许有 2/3 的数组空间去存储数据 超过这个数则需要进行扩容
    dk->dk_usable = USABLE_FRACTION(size); // #define USABLE_FRACTION(n) ((((n) << 1)+1)/3)
    ep0 = &dk->dk_entries[0];
    /* Hash value of slot 0 is used by popitem, so it must be initialized */
    ep0->me_hash = 0;
    // 将所有的键值对初始化成 NULL
    for (i = 0; i < size; i++) {
        ep0[i].me_key = NULL;
        ep0[i].me_value = NULL;
    }
    dk->dk_lookup = lookdict_unicode_nodummy;
    return dk;
}

ハッシュ テーブルの拡張メカニズム

まず、ディクショナリ実装におけるハッシュ テーブルの拡張メカニズムを見てみましょう。ディクショナリに新しいデータを追加し続けると、ディクショナリは間もなく、その中のデータは配列の長さの 23 に達します。この時点で、それを拡張する必要があります。拡張後の配列のサイズは次のように計算されます:

#define GROWTH_RATE(d) (((d)->ma_used*2)+((d)->ma_keys->dk_size>>1))

新しい配列のサイズは次のようになります。元のキーと値のペアの数に 2 を乗算し、元の配列の長さの半分を加えた値。

一般に、拡張には 3 つの主な手順があります。

新しい配列のサイズを計算します。
  • 新しい配列を作成します。
  • 元のハッシュ テーブルのデータを新しい配列に追加します (つまり、再ハッシュ プロセス)。
  • 具体的な実装コードは次のとおりです。
  • static int
    insertion_resize(PyDictObject *mp)
    {
        return dictresize(mp, GROWTH_RATE(mp));
    }
     
    static int
    dictresize(PyDictObject *mp, Py_ssize_t minused)
    {
        Py_ssize_t newsize;
        PyDictKeysObject *oldkeys;
        PyObject **oldvalues;
        Py_ssize_t i, oldsize;
        // 下面的代码的主要作用就是计算得到能够大于等于 minused 最小的 2 的整数次幂
    /* Find the smallest table size > minused. */
        for (newsize = PyDict_MINSIZE_COMBINED;
             newsize <= minused && newsize > 0;
             newsize <<= 1)
            ;
        if (newsize <= 0) {
            PyErr_NoMemory();
            return -1;
        }
        oldkeys = mp->ma_keys;
        oldvalues = mp->ma_values;
        /* Allocate a new table. */
       // 创建新的数组
        mp->ma_keys = new_keys_object(newsize);
        if (mp->ma_keys == NULL) {
            mp->ma_keys = oldkeys;
            return -1;
        }
        if (oldkeys->dk_lookup == lookdict)
            mp->ma_keys->dk_lookup = lookdict;
        oldsize = DK_SIZE(oldkeys);
        mp->ma_values = NULL;
        /* If empty then nothing to copy so just return */
        if (oldsize == 1) {
            assert(oldkeys == Py_EMPTY_KEYS);
            DK_DECREF(oldkeys);
            return 0;
        }
        /* Main loop below assumes we can transfer refcount to new keys
         * and that value is stored in me_value.
         * Increment ref-counts and copy values here to compensate
         * This (resizing a split table) should be relatively rare */
        if (oldvalues != NULL) {
            for (i = 0; i < oldsize; i++) {
                if (oldvalues[i] != NULL) {
                    Py_INCREF(oldkeys->dk_entries[i].me_key);
                    oldkeys->dk_entries[i].me_value = oldvalues[i];
                }
            }
        }
        /* Main loop */
        // 将原来数组当中的元素加入到新的数组当中
        for (i = 0; i < oldsize; i++) {
            PyDictKeyEntry *ep = &oldkeys->dk_entries[i];
            if (ep->me_value != NULL) {
                assert(ep->me_key != dummy);
                insertdict_clean(mp, ep->me_key, ep->me_hash, ep->me_value);
            }
        }
        // 更新一下当前哈希表当中能够插入多少数据
        mp->ma_keys->dk_usable -= mp->ma_used;
        if (oldvalues != NULL) {
            /* NULL out me_value slot in oldkeys, in case it was shared */
            for (i = 0; i < oldsize; i++)
                oldkeys->dk_entries[i].me_value = NULL;
            assert(oldvalues != empty_values);
            free_values(oldvalues);
            DK_DECREF(oldkeys);
        }
        else {
            assert(oldkeys->dk_lookup != lookdict_split);
            if (oldkeys->dk_lookup != lookdict_unicode_nodummy) {
                PyDictKeyEntry *ep0 = &oldkeys->dk_entries[0];
                for (i = 0; i < oldsize; i++) {
                    if (ep0[i].me_key == dummy)
                        Py_DECREF(dummy);
                }
            }
            assert(oldkeys->dk_refcnt == 1);
            DK_DEBUG_DECREF PyMem_FREE(oldkeys);
        }
        return 0;
    }
ディクショナリへのデータの挿入

ディクショナリへのデータの挿入を続けると、おそらく、ハッシュの競合が発生した場合、ハッシュの競合を処理するディクショナリのメソッドは、ハッシュの競合を処理するためにコレクションで使用されるメソッドと基本的に同じです。どちらも開発アドレスのメソッドを使用します。ただし、このオープン アドレスの実装は、この方法は比較的複雑ですが、具体的な手順は以下の通りです。

以上がPython仮想マシンにおける辞書の実装原理は何ですかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。