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

    技术2022-05-11  75

    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


    最新回复(0)