Python源码剖析
——字典对象PyDictObject(3)
本文作者: Robert Chen ([email protected])
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);
}
}
…………
}
现在我们可以根据对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语句的时候,同样会调用dealloc8次,所以每次打印出来,num_free_dicts的值都是8。在后来del d2和del d1时,每次除了Python例行的8大对象的销毁,还有我们自己创建的对象的销毁,所以打印出来的num_free_dicts的值是9和10。