Python源码分析-PyDictObject

1、散列表概述

python中的dict并没有采用map中的红黑树结构做关联,而是使用效率更高的散列表。
散列表通过一个函数将键值映射为一个整数,再将整数作为索引值访问内存。用于映射的函数称为散列函数,映射后的值为散列值。散列会发生冲突,解决散列冲突的方法有很多,python使用的是开放定址法,当发生冲突再次探测可用位置,形成探测链,探测链如果要删掉中间一个元素,会使用伪删除处理,防止链断开搜索失败。

2、PyDictObject

后面将把关联容器中的一个(key, value)元素对称为一个entry或slot。一个entry定义:

typedef struct {
    Py_ssize_t me_hash;
    PyObject *me_key;
    PyObject *me_value;
} PyDictEntry;
me_hash域 存储me_key的散列值,entry分为三种状态:Unused态、Active态、Dummy态,切换如下:

这里写图片描述

3.PyDictObject实际是一堆entry的集合:
typedef struct _dictobject PyDictObject;
struct _dictobject {
    PyObject_HEAD
    Py_ssize_t ma_fill;  /* # Active + # Dummy */
    Py_ssize_t ma_used;  /* # Active */
    Py_ssize_t ma_mask;
    PyDictEntry *ma_table;
    PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
    PyDictEntry ma_smalltable[PyDict_MINSIZE];
};

ma_fill 维护处于Active态和Dummy态的entry数;
ma_used维护处于Active态的entry数;
ma_mask指PyDictObject中所有entry数;
ma_table域 指向PyDictObject中的entry,当其数量小于等于PyDict_MINSIZE(8)时,指向ma_smalltable,否者申请内存指向该内存;

4.PyDictObject对象创建
//[dictobject.c] 
typedef PyDictEntry dictentry;
typedef PyDictObject dictobject;
PyObject *
PyDict_New(void)
{
    register dictobject *mp;
    if (dummy == NULL) { /* Auto-initialize dummy */
//创建PyDictObject时,会先创建一个字符串对象dummy,用作指示标志,表面entry曾被使用,也用于探测序列;
        dummy = PyString_FromString("<dummy key>");
        if (dummy == NULL)
            return NULL;
#ifdef SHOW_CONVERSION_COUNTS
        Py_AtExit(show_counts);
#endif
    }
    //num_free_dicts是dict的缓冲池
    if (num_free_dicts) {
        mp = free_dicts[--num_free_dicts];
        assert (mp != NULL);
        assert (mp->ob_type == &PyDict_Type);
        _Py_NewReference((PyObject *)mp);
        if (mp->ma_fill) {
        //下面看定义
            EMPTY_TO_MINSIZE(mp);
        }
        assert (mp->ma_used == 0);
        assert (mp->ma_table == mp->ma_smalltable);
        assert (mp->ma_mask == PyDict_MINSIZE - 1);
    } else {
        mp = PyObject_GC_New(dictobject, &PyDict_Type);
        if (mp == NULL)
            return NULL;
//然后开始创建,将ma_smalltable、ma_used、ma_fill清0,
//然后ma_table指向ma_smalltable,设置ma_mash,最后将lookdict_string 赋予 ma_lookup。
        EMPTY_TO_MINSIZE(mp);
    }
    mp->ma_lookup = lookdict_string;
#ifdef SHOW_CONVERSION_COUNTS
    ++created;
#endif
    _PyObject_GC_TRACK(mp);
    return (PyObject *)mp;
}
#define EMPTY_TO_MINSIZE(mp) do {                   \
    memset((mp)->ma_smalltable, 0, sizeof((mp)->ma_smalltable));    \
    (mp)->ma_used = (mp)->ma_fill = 0;              \
    INIT_NONZERO_DICT_SLOTS(mp);                    \  //下面看定义
    } while(0)
#define INIT_NONZERO_DICT_SLOTS(mp) do {                \
    (mp)->ma_table = (mp)->ma_smalltable;               \
    (mp)->ma_mask = PyDict_MINSIZE - 1;             \
    } while(0)
5.PyDictObject有两种搜索策略,lookdict和lookdict_string,lookdict_string是lookdict对PyStringObject的特化。
  • 源码lookdict_string:
static dictentry *
lookdict_string(dictobject *mp, PyObject *key, register long hash)
{
    register size_t i;
    register size_t perturb;
    register dictentry *freeslot;
    register size_t mask = (size_t)mp->ma_mask;
    dictentry *ep0 = mp->ma_table;
    register dictentry *ep;
    if (!PyString_CheckExact(key)) {
#ifdef SHOW_CONVERSION_COUNTS
        ++converted;
#endif
        mp->ma_lookup = lookdict;

// lookdict_string是在key为PyStringObject的情况下使用,否则使用lookdict:

        return lookdict(mp, key, hash);
    }
    i = hash & mask;
    ep = &ep0[i];
    if (ep->me_key == NULL || ep->me_key == key)
        return ep;
    if (ep->me_key == dummy)
        freeslot = ep;
    else {
        if (ep->me_hash == hash && _PyString_Eq(ep->me_key, key))
            return ep;
        freeslot = NULL;
    }

    for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
        i = (i << 2) + i + perturb + 1;
        ep = &ep0[i & mask];
        if (ep->me_key == NULL)
            return freeslot == NULL ? ep : freeslot;
        if (ep->me_key == key
            || (ep->me_hash == hash
                && ep->me_key != dummy
            && _PyString_Eq(ep->me_key, key)))
            return ep;
        if (ep->me_key == dummy && freeslot == NULL)
            freeslot = ep;
    }
    assert(0);  /* NOT REACHED */
    return 0;
}
  • 源码lookdict:
static dictentry *
lookdict(dictobject *mp, PyObject *key, register long hash)
{
    register size_t i;
    register size_t perturb;
    register dictentry *freeslot;

//由于PyDictObject中维护dict数量是有限的(ma_table的长度),而计算出的hash值可能超过此范围,
//故需要与ma_mask进行与操作获得下标,因此ma_mask 名字 不是 ma_size。

    register size_t mask = (size_t)mp->ma_mask;
    dictentry *ep0 = mp->ma_table;
    register dictentry *ep;
    register int cmp;
    PyObject *startkey;
//根据hash值获得entry的序号。
    i = (size_t)hash & mask;
    ep = &ep0[i];
//如果ep->me_key为NULL,且与key相同,搜索失败。
    if (ep->me_key == NULL || ep->me_key == key)
        return ep;

//freeslot用来指向第一次搜索序列中的Dummy态entry,
    if (ep->me_key == dummy)
        freeslot = ep;
    else {
    //检查当前Active的entry中的key与待查找的key是否相同,如果相同,则立即返回,搜索成功。
        if (ep->me_hash == hash) {
            startkey = ep->me_key;
            Py_INCREF(startkey);
//函数原形为:
//int PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
//当(v op w)成立时,返回1;当(v op w)不成立时,返回0;如果在比较中发生错误,则返回-1。
            cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
            Py_DECREF(startkey);
            if (cmp < 0)
                return NULL;
            if (ep0 == mp->ma_table && ep->me_key == startkey) {
                if (cmp > 0)
                    return ep;
            }
            else {
                return lookdict(mp, key, hash);
            }
        }
        freeslot = NULL;
    }

    for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
    获得探测序列中的下一个待探测的entry。
        i = (i << 2) + i + perturb + 1;
        ep = &ep0[i & mask];
//ep到达一个Unused态entry,表明搜索结束。这是如果freeslot不为空,则返回freeslot所指entry。
        if (ep->me_key == NULL)
            return freeslot == NULL ? ep : freeslot;
//entry与待查找的key匹配,搜索成功。
        if (ep->me_key == key)
            return ep;

        if (ep->me_hash == hash && ep->me_key != dummy) {
            startkey = ep->me_key;
            Py_INCREF(startkey);

            cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
            Py_DECREF(startkey);
            if (cmp < 0)
                return NULL;
            if (ep0 == mp->ma_table && ep->me_key == startkey) {
                if (cmp > 0)
                    return ep;
            }
            else {
                return lookdict(mp, key, hash);
            }
        }
//在探测序列中发现Dummy态entry,设置freeslot。

        else if (ep->me_key == dummy && freeslot == NULL)
            freeslot = ep;
    }
    assert(0);  /* NOT REACHED */
    return 0;
}

比较lookdict_string与lookdict可发现,lookdict_string是lookdict针对PyStringObject的简化版,而且效率要高很多。Python自身也大量使用PyDictObject对象,大都使用PyStringObject作为key,故lookdict_string对Python整理运行效率都有重要影响。

6.插入与删除: PyDictObject插入建立在搜索上:
static int
insertdict(register dictobject *mp, PyObject *key, long hash, PyObject *value)
{
    PyObject *old_value;
    register dictentry *ep;
    typedef PyDictEntry *(*lookupfunc)(PyDictObject *, PyObject *, long);

    assert(mp->ma_lookup != NULL);
    ep = mp->ma_lookup(mp, key, hash);
    if (ep == NULL) {
        Py_DECREF(key);
        Py_DECREF(value);
        return -1;
    }
//搜索结果可能是Active态的entry,也可能是Dummy或Unused态的entry;
//对于前者只需替换me_value,对于后者要设置其他值:
//搜索成功,返回处于Active的entry,直接替换me_value。
    if (ep->me_value != NULL) {
        old_value = ep->me_value;
        ep->me_value = value;
        Py_DECREF(old_value); /* which **CAN** re-enter */
        Py_DECREF(key);
    }
    else {
//搜索失败,返回Unused或Dummy的entry,完整设置me_key,me_hash和me_value。
        if (ep->me_key == NULL)
            mp->ma_fill++;
        else {
            assert(ep->me_key == dummy);
            Py_DECREF(dummy);
        }
        ep->me_key = key;
        ep->me_hash = (Py_ssize_t)hash;
        ep->me_value = value;
        mp->ma_used++;
    }
    return 0;
}
在调用insertdict前会调用 PyDict_SetItem:
int
PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
{
    register dictobject *mp;
    register long hash;
    register Py_ssize_t n_used;

    if (!PyDict_Check(op)) {
        PyErr_BadInternalCall();
        return -1;
    }
    assert(key);
    assert(value);
    mp = (dictobject *)op;
//计算hash值 
    if (PyString_CheckExact(key)) {
        hash = ((PyStringObject *)key)->ob_shash;
        if (hash == -1)
            hash = PyObject_Hash(key);
    }
    else {
        hash = PyObject_Hash(key);
        if (hash == -1)
            return -1;
    }
    assert(mp->ma_fill <= mp->ma_mask);  /* at least one empty slot */
    n_used = mp->ma_used;
    Py_INCREF(value);
    Py_INCREF(key);
//调用insertdict方法,将计算得到的hash带入
    if (insertdict(mp, key, hash, value) != 0)
        return -1;
/*
在插入元素后会判断是否需要改变ma_table大小。判断条件为装载率大于2/3((mp->ma_fill)/(mp->ma_mask+1) >= 2/3)
而且使用了Unused态的entry(mp->ma_used > n_used)。在改变table时可能是增加也可能是减少,
新增大小为table中Active态的entry数的2或4倍(看数量是否超过50000)。
*/
    if (!(mp->ma_used > n_used && mp->ma_fill*3 >= (mp->ma_mask+1)*2))
        return 0;
    return dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used);
}
改变table大小则由dictresize负责:
static int
dictresize(dictobject *mp, Py_ssize_t minused)
{
    Py_ssize_t newsize;
    dictentry *oldtable, *newtable, *ep;
    Py_ssize_t i;
    int is_oldtable_malloced;
    dictentry small_copy[PyDict_MINSIZE];

    assert(minused >= 0);
/*
dictresize首先会确定新的table的大小,很显然,这个大小一定要大于传入的参数minused,
这也是在原来的table中处于Active态的entry的数量。dictresize从8开始,以指数方式增加大小,
直到超过了minused为止。所以实际上新的table的大小在大多数情况下至少是原来table中Active态entry数量的4倍。
*/
    for (newsize = PyDict_MINSIZE;
         newsize <= minused && newsize > 0;
         newsize <<= 1)
        ;
    if (newsize <= 0) {
        PyErr_NoMemory();
        return -1;
    }

    oldtable = mp->ma_table;
    assert(oldtable != NULL);
    is_oldtable_malloced = oldtable != mp->ma_smalltable;
/*
如果在[1]中获得的新的table大小为8,则不需要在堆上分配空间,
直接使用ma_smalltable就可以了;否则,则需要在堆上分配空间。
*/
    if (newsize == PyDict_MINSIZE) {
        newtable = mp->ma_smalltable;
        if (newtable == oldtable) {
            if (mp->ma_fill == mp->ma_used) {
                /* No dummies, so no point doing anything. */
                return 0;
            }
            assert(mp->ma_fill > mp->ma_used);
            memcpy(small_copy, oldtable, sizeof(small_copy));
            oldtable = small_copy;
        }
    }
    else {
        newtable = PyMem_NEW(dictentry, newsize);
        if (newtable == NULL) {
            PyErr_NoMemory();
            return -1;
        }
    }

/*
对新的table进行初始化,并调整原来PyDictObject对象中用于维护table使用情况的变量。
*/
    assert(newtable != oldtable);
    mp->ma_table = newtable;
    mp->ma_mask = newsize - 1;
    memset(newtable, 0, sizeof(dictentry) * newsize);
    mp->ma_used = 0;
    i = mp->ma_fill;
    mp->ma_fill = 0;

/*
对原来table中的非Unused态entry进行处理。对于Active态entry,显然需要将其插入到新的table中,
这个动作由前面考察过的insertdict完成;而对于Dummy态的entry,则略过,
不做任何处理,因为我们知道Dummy态entry存在的唯一理由就是为了不使搜索时的探测序列中断。
现在所有Active态的entry都重新依次插入新的table中,它们会形成一条新的探测序列,不再需要这些Dummy态的entry了。
*/
    for (ep = oldtable; i > 0; ep++) {
        if (ep->me_value != NULL) { /* active entry */
            --i;
            insertdict_clean(mp, ep->me_key, (long)ep->me_hash,
                     ep->me_value);
        }
        else if (ep->me_key != NULL) {  /* dummy entry */
            --i;
            assert(ep->me_key == dummy);
            Py_DECREF(ep->me_key);
        }
        /* else key == value == NULL:  nothing to do */
    }

    if (is_oldtable_malloced)
        PyMem_DEL(oldtable);
    return 0;
}
7.从PyDictObject中删除一个元素(先获取hash值,取到entry后将entry从Active态转为Dummy态,再调整相关变量):
int
PyDict_DelItem(PyObject *op, PyObject *key)
{
    register dictobject *mp;
    register long hash;
    register dictentry *ep;
    PyObject *old_value, *old_key;

    if (!PyDict_Check(op)) {
        PyErr_BadInternalCall();
        return -1;
    }
    assert(key);
      //获得hash值 
    if (!PyString_CheckExact(key) ||
        (hash = ((PyStringObject *) key)->ob_shash) == -1) {
        hash = PyObject_Hash(key);
        if (hash == -1)
            return -1;
    }

    //搜索entry 
    mp = (dictobject *)op;
    ep = (mp->ma_lookup)(mp, key, hash);
    if (ep == NULL)
        return -1;
    if (ep->me_value == NULL) {
        set_key_error(key);
        return -1;
    }

    //删除entry所维护的元素 
    old_key = ep->me_key;
    Py_INCREF(dummy);
    ep->me_key = dummy;
    old_value = ep->me_value;
    ep->me_value = NULL;
    mp->ma_used--;
    Py_DECREF(old_value);
    Py_DECREF(old_key);
    return 0;
}
8、PyDictObject 对象缓冲池

PyDictObject和PyListObject一样也使用缓冲池技术:

缓冲池中只保留了PyDictObject对象,里面从堆上申请的table则会被销毁,归还系统。如果被销毁的PyDictObject对象只是用了固有的ma_smalltable,那只需调整ma_smalltable中对象的引用计数。

[dictobject.c] 
#define MAXFREEDICTS 80 

static PyDictObject *free_dicts[MAXFREEDICTS]; 

static int num_free_dicts = 0;
  • 而且和PyListObject的缓冲池类似,在PyDictObject对象被销毁时才把内存加入缓冲池:
static void
dict_dealloc(register dictobject *mp)
{
    register dictentry *ep;
    Py_ssize_t fill = mp->ma_fill;
    PyObject_GC_UnTrack(mp);
    Py_TRASHCAN_SAFE_BEGIN(mp)
    for (ep = mp->ma_table; fill > 0; ep++) {
        if (ep->me_key) {
            --fill;
//调整dict中对象的引用计数 
            Py_DECREF(ep->me_key);
            Py_XDECREF(ep->me_value);
        }
    }

//向系统归还从堆上申请的空间 
    if (mp->ma_table != mp->ma_smalltable)
        PyMem_DEL(mp->ma_table);
//将被销毁的PyDictObject对象放入缓冲池 
    if (num_free_dicts < MAXFREEDICTS && mp->ob_type == &PyDict_Type)
        free_dicts[num_free_dicts++] = mp;
    else
        mp->ob_type->tp_free((PyObject *)mp);
    Py_TRASHCAN_SAFE_END(mp)
}
在创建PyDictObject对象时,缓冲池有则直接从缓冲池取:
[dictobject.c] 

PyObject* PyDict_New(void) 

{ 

register dictobject *mp; 

………… 

    if (num_free_dicts) { 

        mp = free_dicts[--num_free_dicts]; 

        _Py_NewReference((PyObject *)mp); 

        if (mp->ma_fill) { 

            EMPTY_TO_MINSIZE(mp); 

        } 

} 

………… 

}
python内部大量使用PyDictObject,每个小小调用都会对insertdict频繁调用,故打印的话可用特征串,打印: 调用print的时候也会调用到dealloc,所以num_free_dicts的值变化可能和想象的不一样。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值