您当前的位置:首页 > 计算机 > 编程开发 > Python

Python 源码剖析 - 字典对象 PyDictObject 4

时间:12-14来源:作者:点击数:

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对象,依次插入9和17,根据PyDictObject选用的hash策略,这两个数会产生冲突,9的hash结果为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。有一点奇怪的是,在创建了d2和d3之后,num_free_dicts 的值仍然都是8。直觉上来讲,它们对应的是应该是6和5才对。但是看一看左边的图9,其实在执行 print 语句的时候,同样会调用 dealloc 8 次,所以每次打印出来,num_free_dicts 的值都是8。在后来 del d2 和 del d1 时,每次除了 Python 例行的8大对象的销毁,还有我们自己创建的对象的销毁,所以打印出来的 num_free_dicts 的值是9和10。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐