Python源码剖析[14] —— 字典对象PyDictObject(3)

来源:互联网 发布:大数据 企业风险管理 编辑:程序博客网 时间:2024/06/01 10:45

Python源码剖析

——字典对象PyDictObject(3)

本文作者: Robert Chen (search.pythoner@gmail.com)

4         PyDictObject对象缓冲池

前面我们提到,在PyDictObject的实现机制中,同样使用了缓冲池的技术:

[dictobject.c]

#define MAXFREEDICTS 80

static PyDictObject *free_dicts[MAXFREEDICTS];

static int num_free_dicts = 0;

 

 

实际上PyDictObject中使用的这个缓冲池机制与PyListObject中使用的缓冲池机制是一样的。开始时,这个缓冲池里什么都没有,直到有第一个PyDictObject被销毁时,这个缓冲池才开始接纳被缓冲的PyDictObject对象:

[dictobject.c]

static void dict_dealloc(register dictobject *mp)

{

    register dictentry *ep;

    int fill = mp->ma_fill;

    PyObject_GC_UnTrack(mp);

Py_TRASHCAN_SAFE_BEGIN(mp)

//调整dict中对象的引用计数

    for (ep = mp->ma_table; fill > 0; ep++) {

        if (ep->me_key) {

            --fill;

            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)

}

 

 

PyListObject中缓冲池的机制一样,缓冲池中只保留了PyDictObject对象,而PyDictObject对象中维护的从堆上申请的table的空间则被销毁,并归还给系统了。具体原因参见PyListObject的讨论。而如果被销毁的PyDictObject中的table实际上并没有从系统堆中申请,而是指向PyDictObject固有的ma_smalltable,那么只需要调整ma_smalltable中的对象引用计数就可以了。

在创建新的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);

        }

}

…………

}

5         Hack PyDictObject

现在我们可以根据对PyDictObject的了解,在Python源代码中添加代码,动态而真实地观察Python运行时PyDictObject的一举一动了。

我们首先来观察,在insertdict发生之后,PyDictObject对象中table的变化情况。由于Python内部大量地使用PyDictObject,所以对insertdict的调用会非常频繁,成千上万的PyDictObject对象会排着长队来依次使用insertdict。如果只是简单地输出,我们立刻就会被淹没在输出信息中。所以我们需要一套机制来确保当insertdict发生在某一特定的PyDictObject对象身上时,才会输出信息。这个PyDictObject对象当然是我们自己创建的对象,必须使它有区别于Python内部使用的PyDictObject对象的特征。这个特征,在这里,我把它定义为PyDictObject包含“Python_Robert”的PyStringObject对象,当然,你也可以选用自己的特征串。如果在PyDictObject中找到了这个对象,则输出信息。

static void ShowDictObject(dictobject* dictObject)

{

   dictentry* entry = dictObject->ma_table;

   int count = dictObject->ma_mask+1;

   int i;

   for(i = 0; i < count; ++i)

   {

      PyObject* key = entry->me_key;

      PyObject* value = entry->me_value;

      if(key == NULL)

      {

         printf("NULL");

      }

      else

      {

         (key->ob_type)->tp_print(key, stdout, 0);

      }

 

 

      printf("/t");

 

 

      if(value == NULL)

      {

         printf("NULL");

      }

      else

      {

         (key->ob_type)->tp_print(value, stdout, 0);

      }

      printf("/n");

      ++entry;

   }

}

static void

insertdict(register dictobject *mp, PyObject *key, long hash, PyObject *value)

{

    ……

   {

      dictentry *p;

      long strHash;

      PyObject* str = PyString_FromString("Python_Robert");

      strHash = PyObject_Hash(str);

      p = mp->ma_lookup(mp, str, strHash);

      if(p->me_value != NULL && (key->ob_type)->tp_name[0] == 'i')

      {

         PyIntObject* intObject = (PyIntObject*)key;

         printf("insert %d/n", intObject->ob_ival);

 

 

         ShowDictObject(mp);

      }

   }

}                                                                        

对于PyDictObject对象,依次插入917,根据PyDictObject选用的hash策略,这两个数会产生冲突,9hash结果为1,而17经过再次探测后,会获得hash结果为7。图7是观察结果:

                               

                               

                               

然后将9删除,则原来9的位置会出现一个dummy态的标识。然后将17删除,并再次插入17,显然,17应该出现在原来9的位置,而原来17的位置则是dummy标识。图8是观察结果。

下面我们观察Python内部对PyDictObject的使用情况,在dict_dealloc中添加代码监控Python在执行时调用dict_dealloc的频度,图9是监测结果。

我们前面已经说了,Python内部大量使用了PyDictObject对象,然而监测的结果还是让我们惊讶不已,原来对于一个简简单单的赋值,一个简简单单的打印,Python内部都会创建并销毁多达8个的PyDictObject对象。不过这其中应该有参与编译的PyDictObject对象,所以在执行一个完整的Python源文件时,并不是每一行都会有这样的八仙过海 :)当然,我们可以看到,这些PyDictObject对象中entry的个数都很少,所以只需要使用ma_smalltable就可以了。这里,也指出了PyDictObject缓冲池的重要性。

 

 

所以我们也监控了缓冲池的使用,在dict_print中添加代码,打印当前的num_free_dicts值。监控结果见图10。有一点奇怪的是,在创建了d2d3之后,num_free_dicts的值仍然都是8。直觉上来讲,它们对应的是应该是65才对。但是,但是,:),看一看左边的图9,其实在执行print语句的时候,同样会调用dealloc8次,所以每次打印出来,num_free_dicts的值都是8。在后来del d2del d1时,每次除了Python例行的8大对象的销毁,还有我们自己创建的对象的销毁,所以打印出来的num_free_dicts的值是910