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     /* "\nnumfree: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) + "]\n" + "Yes:xx\n"|"No1234\n" */
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, "\nYes:%2d", free_list_index);
67     else
68         snprintf(test_message, 8, "\nNo    ");
69     if(_PyUnicodeWriter_WriteASCIIString(&writer, test_message, 7) < 0)
70         goto error;
71
72     writer.overallocate = 0;
73     snprintf(test_message1, 12, "\nnumfree:%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已经不在对象池中了,
也就是发生覆盖时会把被覆盖的对象移出对象池。

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

时间: 2024-10-15 14:26:18

CPython对象模型:List的相关文章

CPython对象模型:整型

前一篇:CPython对象模型:基础 程序中,最常用的数据类型之一就是整型了. 本篇博文记录的就是研究整型过程中的一些心得. 1 PyLongObject 1.1 版本之别 在python2.x中,整型对象还有两种:不太大的整数int(约等于C语言中long)和大整数long. 在python3之后,这两种类型合并为int,但新的int类型的表现和2.x中的long其实更为接近. 在python2.x中,int是一个定长的类型,并且采用了两个不同的内存池分别存放小整数和大整数: 但在python

CPython对象模型:Dict

此系列前几篇: CPython对象模型:基础 CPython对象模型:整型 CPython对象模型:string CPython对象模型:List 除了list以外,dict也是python中十分常用的一种基本数据结构. 而且,dict在python内部被大量应用, dict的效率会直接影响python的运行效率, 因此python的作者们对dict进行了精心的设计和优化. 本篇博客会从源码出发仔细分析一下python中的dict. 1 结构 由于对dict的效率有着严格要求, python中的

CPython对象模型:基础

1 前言 最近在读<python源码剖析>一书,收获颇丰. 虽然此书成书已久,书中所讲与如今的实现已有颇多不同, 可是程序框架并未有太多改动,再辅以python官网文档, 仍可以借此一窥python源码. 在依据此书参研过程中,所获颇丰,淋漓尽致之余突生记录心得之想,因此开始写这篇博客. 如果我懒癌没发作,那么应该会有若干后续博客,或许会涉及python大部分内置类型的剖析乃至其他: 若不幸懒癌发作,这一篇博客也足以记录足够的知识点以便以后快速想起对象模型相关. 文中代码的依据均是 Pytho

CPython对象模型:string(留坑待填)

在python3中,移除了2中的byte string,string变的和2中的unicode类似.所以在python3中烦人的编码问题会少不少. 在准备动手写这一篇的时候,查了不少资料,结果不小心发现了PEP-393 这个是unicode部分的原作者亲自写的,内容详细解释的很清楚,只要对python对象有个基础的认识绝对看得懂啊! 于是为了先赶赶进度,我决定先给string留个坑,等以后再填……

【Python笔记】从一个“古怪”的case探究CPython对Int对象的实现细节

1. Python的对象模型 我们知道,在Python的世界里,万物皆对象(Object).根据Python官方文档对Data Model的说明,每个Python对象均拥有3个特性:身份.类型和值. 官方文档关于对象模型的这段概括说明对于我们理解Python对象是如此重要,所以本文将其摘录如下(为了使得结构更清晰,这里把原文档做了分段处理): 1) Every object has an identity, a type and a value. 2) An object's identity

JavaScript----BOM(浏览器对象模型)

BOM 浏览器对象模型 BOM 的全称为 Browser Object Model,被译为浏览器对象模型.BOM提供了独立于 HTML 页面内容,而与浏览器相关的一系列对象.主要被用于管理浏览器窗口及与浏览器窗口之间通信等功能. 1.Window 对象 window对象是BOM中最顶层对象:表示当前浏览器窗口,window对象的属性和方法应用于当前整个浏览器窗口. window 对象的属性主要有: screenX / screenY / screenLeft / screenTop:都为获取位置

C++ 继承、多继承、虚拟继承对象模型

C++面向对象语言一大难点是继承,但又是不得不掌握的.简单的继承是很容易理解的,但是当涉及到多继承,设计到虚函数的继承,特别是涉及到虚继承时,问题就会变得复杂.下面的内容来自参考资料中的三篇文章.C++的继承学习中,最主要是要掌握派生类的对象模型,基类和派生类指针之间的向上向下类型转换,当继承中的出现虚函数成员函数的访问(多态),虚继承是如何通过引入虚基表解决"菱形继承"中存在多份公共基类的问题. 一.简单的对象模型 1.定义 class MyClass { public: int v

第五十课、c++对象模型分析(上)

一.c++对象模型之成员变量 1.class是一种特殊的struct (1).在内存中class依旧可以看做是变量的集合 (2).class与struct遵循相同的内存对齐规则 (3).class中的成员函数和成员变量是分开存储的 A.每个对象有独立的成员变量 B.所有对象共享类中的成员函数 2.运行时的对象退化为结构体的形式 (1).所有成员变量在内存中依次分布 (2).成员变量间可能存在内存间隙 (3).可以通过内存地址直接访问成员变量 (4).访问权限关键字在运行时失效 #include<

第51课 C++对象模型分析(下)

1. 单继承对象模型 (1)单一继承 [编程实验]继承对象模型初探 #include <iostream> using namespace std; class Demo { protected: int mi; int mj; public: //虚函数 virtual void print() { cout << "mi = " << mi << ", " << "mj = " &l