• CPython对象模型:List


    此系列前几篇:

    CPython对象模型:基础

    CPython对象模型:整型

    CPython对象模型:string


    list是一种经常用到的数据结构,在python中常使用list来构造高级的数据结构。 本文记录了我对list对象的解析所得。

    1 PyListObject

    首先,来看看PyListObject的定义:

    
    
    typedef struct {
        PyObject_VAR_HEAD
        PyObject **ob_item;
        Py_ssize_t allocated;
    } PyListObject;
    
    
    

    这个定义很简单,变长对象头,PyObject*类型的数组ob_item用来存放数据的引用, 加上分配内存的大小allocated就构成了PyListObject。

    allocated和变长头中的ob_size的区别在于,allocated是已分配区域能容纳的最大对象数, 而ob_size则是list中已放入的对象数。 这样设定的原因也很简单,如果每次都申请恰到好处的空间会使得插入删除等操作变的十分低效, 因此list会申请大一点的空间来提高操作效率。 不难得出,存在以下关系:

    0 ≤ ob_size ≤ allocated
    len(list) = ob_size

    2 创建、维护

    2.1 创建

    创建函数PyList_New定义如下:

     1 /* file:Objects/listobject.c */
     2 PyObject *
     3 PyList_New(Py_ssize_t size)
     4 {
     5     PyListObject *op;
     6     size_t nbytes;
     7 #ifdef SHOW_ALLOC_COUNT
     8     static int initialized = 0;
     9     if (!initialized) {
    10         Py_AtExit(show_alloc);
    11         initialized = 1;
    12     }
    13 #endif
    14 
    15     if (size < 0) {
    16         PyErr_BadInternalCall();
    17         return NULL;
    18     }
    19     /* Check for overflow without an actual overflow,
    20      *  which can cause compiler to optimise out */
    21     if ((size_t)size > PY_SIZE_MAX / sizeof(PyObject *))
    22         return PyErr_NoMemory();
    23     nbytes = size * sizeof(PyObject *);
    24     if (numfree) {
    25         numfree--;
    26         op = free_list[numfree];
    27         _Py_NewReference((PyObject *)op);
    28 #ifdef SHOW_ALLOC_COUNT
    29         count_reuse++;
    30 #endif
    31     }
    32     else {
    33         op = PyObject_GC_New(PyListObject, &PyList_Type);
    34         if (op == NULL)
    35             return NULL;
    36 #ifdef SHOW_ALLOC_COUNT
    37         count_alloc++;
    38 #endif
    39     }
    40     if (size <= 0)
    41         op->ob_item = NULL;
    42     else {
    43         op->ob_item = (PyObject **) PyMem_MALLOC(nbytes);
    44         if (op->ob_item == NULL) {
    45             Py_DECREF(op);
    46             return PyErr_NoMemory();
    47         }
    48         memset(op->ob_item, 0, nbytes);
    49     }
    50     Py_SIZE(op) = size;
    51     op->allocated = size;
    52     _PyObject_GC_TRACK(op);
    53     return (PyObject *) op;
    54 }

    21~22行进行了溢出检查。

    24~27行是从对象池free_list中获取对象。如果对象池内有初始化好的对象, 那么就直接使用此对象。关于对象池的初始化会在后面提到。

    32~35行则是创建新的对象。PyObject_GC_New是一个用来分配内存的函数。

    40~53行则是在新建list对象后最近若干初始化并返回建好的list对象。 不难发现,ob_size为0时ob_item为NULL。

    2.2 调整大小

     1 /* file:Objects/listobject.c */
     2 static int
     3 list_resize(PyListObject *self, Py_ssize_t newsize)
     4 {
     5     PyObject **items;
     6     size_t new_allocated;
     7     Py_ssize_t allocated = self->allocated;
     8 
     9     /* Bypass realloc() when a previous overallocation is large enough
    10        to accommodate the newsize.  If the newsize falls lower than half
    11        the allocated size, then proceed with the realloc() to shrink the list.
    12     */
    13     if (allocated >= newsize && newsize >= (allocated >> 1)) {
    14         assert(self->ob_item != NULL || newsize == 0);
    15         Py_SIZE(self) = newsize;
    16         return 0;
    17     }
    18 
    19     /* This over-allocates proportional to the list size, making room
    20      * for additional growth.  The over-allocation is mild, but is
    21      * enough to give linear-time amortized behavior over a long
    22      * sequence of appends() in the presence of a poorly-performing
    23      * system realloc().
    24      * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
    25      */
    26     new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
    27 
    28     /* check for integer overflow */
    29     if (new_allocated > PY_SIZE_MAX - newsize) {
    30         PyErr_NoMemory();
    31         return -1;
    32     }
    33     else {
    34         new_allocated += newsize;
    35     }
    36 
    37     if (newsize == 0)
    38         new_allocated = 0;
    39     items = self->ob_item;
    40     if (new_allocated <= (PY_SIZE_MAX / sizeof(PyObject *)))
    41         PyMem_RESIZE(items, PyObject *, new_allocated);
    42     else
    43         items = NULL;
    44     if (items == NULL) {
    45         PyErr_NoMemory();
    46         return -1;
    47     }
    48     self->ob_item = items;
    49     Py_SIZE(self) = newsize;
    50     self->allocated = new_allocated;
    51     return 0;
    52 }

    调整大小时,当newsize满足 allocated/2 ≤ newsize ≤ allocated时, 此函数只会调整ob_size的大小,不会重新分配内存(13~17行)。

    在不满足上述条件时,新的大小 new_allocated = newsize/8 + (newsize < 9 ? 3 : 6) + newsize。 如果newsize为0那么new_allocated也是0(26~38行)。

    重新分配内存后,使ob_size=new_size,allocated=new_allocated, 调整大小的操作就结束了。

    2.3 对象池

    list对象池并不是开始就初始化好的,而是动态初始化的。 初始化的过程发生在list_dealloc函数中:

    
    
     1 /* file:Object/listobject.c */
     2 static void
     3 list_dealloc(PyListObject *op)
     4 {
     5     Py_ssize_t i;
     6     PyObject_GC_UnTrack(op);
     7     Py_TRASHCAN_SAFE_BEGIN(op)
     8         if (op->ob_item != NULL) {
     9             /* Do it backwards, for Christian Tismer.
    10                There's a simple test case where somehow this reduces
    11                thrashing when a *very* large list is created and
    12                immediately deleted. */
    13             i = Py_SIZE(op);
    14             while (--i >= 0) {
    15                 Py_XDECREF(op->ob_item[i]);
    16             }
    17             PyMem_FREE(op->ob_item);
    18         }
    19     if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
    20         free_list[numfree++] = op;
    21     else
    22         Py_TYPE(op)->tp_free((PyObject *)op);
    23     Py_TRASHCAN_SAFE_END(op)
    24 }
    
    
    
    
    

    初始化对象池发生在19~20行。当一个list被释放时, 如果free_list并未满,那么就把这个即将被释放的对象放入对象池中。 也就是说,free_list中的对象都是已经死去的list的遗体。 这样的好处是避免了频繁的内存操作,提高了效率。

    需要注意的是,放入对象池的仅有list对象本身, ob_item对应的数据区域不会保留,而会被释放。 虽然把数据区保留可以更大的提高效率, 可是空间浪费会更严重。

    3 Hack it及疑问

    3.1 Hack it

    为了可以看到更多信息,我们可以修改PyList_Type的tp_str 成员来改变print函数的行为。

    list的tp_str成员为NULL,因此会调用repr函数。 可是修改repr会导致编译python时发生错误,因此我们需要给list加一个str函数。 可以通过复制repr函数并进行修改来快速的写出我们需要的str函数。

    以这样的问题为例: 验证一个新建的list是否在对象池中。 如果在对象池内,输出Yes和其在free_list中的下标; 如果不在对象池内,则输出No。 最后输出numfree的值。

    思路很简单,在str内遍历检查free_list,如果内容和需要打印的list对象相等, 则保存下标。

    我的做法如下:

     1 /* file:Objects/listobject.c */
     2 static PyObject *
     3 list_str(PyListObject *v)
     4 {
     5     Py_ssize_t i;
     6     PyObject *s;
     7     _PyUnicodeWriter writer;
     8     int free_list_index;
     9     int cached = 0;
    10     /* "Yes:xx" or "No" */
    11     char test_message[8] = {0};
    12     /* "
    numfree:xx" */
    13     char test_message1[12] = {0};
    14 
    15     if (Py_SIZE(v) == 0) {
    16         return PyUnicode_FromString("[]");
    17     }
    18 
    19     i = Py_ReprEnter((PyObject*)v);
    20     if (i != 0) {
    21         return i > 0 ? PyUnicode_FromString("[...]") : NULL;
    22     }
    23 
    24     /* Check whether the list is in free_list */
    25     for(free_list_index = 0; free_list_index < PyList_MAXFREELIST; ++free_list_index)
    26         if(v == free_list[free_list_index])
    27         {
    28             cached = 1;
    29             break;
    30         }
    31 
    32     _PyUnicodeWriter_Init(&writer);
    33     writer.overallocate = 1;
    34     /* "[" + "1" + ", 2" * (len - 1) + "]
    " + "Yes:xx
    "|"No1234
    " */
    35     writer.min_length = 1 + 1 + (2 + 1) * (Py_SIZE(v) - 1) + 9;
    36 
    37     if (_PyUnicodeWriter_WriteChar(&writer, '[') < 0)
    38         goto error;
    39 
    40     /* Do repr() on each element.  Note that this may mutate the list,
    41        so must refetch the list size on each iteration. */
    42     for (i = 0; i < Py_SIZE(v); ++i) {
    43         if (i > 0) {
    44             if (_PyUnicodeWriter_WriteASCIIString(&writer, ", ", 2) < 0)
    45                 goto error;
    46         }
    47 
    48         if (Py_EnterRecursiveCall(" while getting the repr of a list"))
    49             goto error;
    50         s = PyObject_Repr(v->ob_item[i]);
    51         Py_LeaveRecursiveCall();
    52         if (s == NULL)
    53             goto error;
    54 
    55         if (_PyUnicodeWriter_WriteStr(&writer, s) < 0) {
    56             Py_DECREF(s);
    57             goto error;
    58         }
    59         Py_DECREF(s);
    60     }
    61 
    62     if (_PyUnicodeWriter_WriteChar(&writer, ']') < 0)
    63         goto error;
    64 
    65     if(cached)
    66         snprintf(test_message, 8, "
    Yes:%2d", free_list_index);
    67     else
    68         snprintf(test_message, 8, "
    No    ");
    69     if(_PyUnicodeWriter_WriteASCIIString(&writer, test_message, 7) < 0)
    70         goto error;
    71 
    72     writer.overallocate = 0;
    73     snprintf(test_message1, 12, "
    numfree:%2d", numfree);
    74     if(_PyUnicodeWriter_WriteASCIIString(&writer, test_message1, 11) < 0)
    75         goto error;
    76 
    77     Py_ReprLeave((PyObject *)v);
    78     return _PyUnicodeWriter_Finish(&writer);
    79 
    80 error:
    81     _PyUnicodeWriter_Dealloc(&writer);
    82     Py_ReprLeave((PyObject *)v);
    83     return NULL;
    84 }

    3.2 问题

    再看看针对对象池的操作,会发现一个问题。 填充对象池时,会把废弃的list对象填入free_list[numfree++]中; 使用对象池中的对象时,使用free_list[–numfree]中的对象。 也就是说,填充新的对象进对象池时,会把对象填入numfree对应的位置; 使用时,则使用numfree的前一个对象。 在使用对象池中的对象后,numfree对应的则是一个已被使用的对象。 如果再有一个新的list对象废弃,那么这个对象填入numfree对应位置, 就会覆盖已使用的对象。

    为了验证这个问题是否存在,可以使用前文给出的str函数。 在交互模式下进行了若干实验后,我发现numfree的值并未发生改变。 修改了str函数让它打印引用数后,可以发现还有其他东西引用了该list,所以更改变量的引用对象也不会触发空间的释放;而使用del显式删除list后,numfree的值仍不改变,真是奇怪。

    于是我想试试非交互模式下python的行为。测试代码很简单:

    
    
    a = [1, 2, 3, 4]
    b = [1, 2, 3]
    c = [1, 2]
    print("a = {}".format(a));
    print("b = {}".format(b));
    print("c = {}".format(c));
    del(a);
    del(b);
    print("c = {}".format(c));
    
    
    
    

    测试结果如下:

    a = [1, 2, 3, 4]
    Yes: 4
    numfree: 1
    ref:5
    b = [1, 2, 3]
    Yes: 3
    numfree: 1
    ref:5
    c = [1, 2]
    Yes: 2
    numfree: 1
    ref:5
    c = [1, 2]
    No
    numfree: 3
    ref:5

    可以看到,c一开始的index是2。当删除b时,numfree值为2,会覆盖c; 这时神奇的事情发生了,再次打印c发现c已经不在对象池中了, 也就是发生覆盖时会把被覆盖的对象移出对象池。

    这个神奇的操作保证了覆盖不会引发问题。 具体的操作源码在哪里我尚未找到,等我找到再对它进行详细的分析。 不过最起码,可以松一口气,不用担心对象池中覆盖导致的问题了。

  • 相关阅读:
    Ubuntu 拦截并监听 power button 的关机消息
    Android 电池管理系统架构总结 Android power and battery management architecture summaries
    Linux 内核代码风格
    Linux 内核工作队列之work_struct 学习总结
    微信小程序 登录流程规范解读
    微信小程序监听input输入并取值
    koala 编译scss不支持中文(包括中文注释),解决方案如下
    阻止冒泡和阻止默认事件的兼容写法
    使用setTimeout实现setInterval
    css实现视差滚动效果
  • 原文地址:https://www.cnblogs.com/w0mTea/p/4295864.html
Copyright © 2020-2023  润新知