此系列前几篇:
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已经不在对象池中了, 也就是发生覆盖时会把被覆盖的对象移出对象池。
这个神奇的操作保证了覆盖不会引发问题。 具体的操作源码在哪里我尚未找到,等我找到再对它进行详细的分析。 不过最起码,可以松一口气,不用担心对象池中覆盖导致的问题了。