目录
1.初识PyListObject
python里的列表不是书上基于链表的列表,而是基于可变长度的数组。PyListObject对象可以有效支持元素插入添加删除等操作,它的定义为:
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
ob_item指针指向元素列表所在内存块首地址,而allocated中则维护了当前列表中可容纳的元素总数。我们知道PyObject_VAR_HEAD中有一个ob_size,它维护的是当前列表中已容纳的元素数量。
2.PyListObject对象的创建和维护
2.1创建对象
PyObject *PyList_New(Py_ssize_t size)
{
PyListObject *op;
static int initialized = 0;
if (!initialized) {
Py_AtExit(show_alloc);
initialized = 1;
}
#endif
if (size < 0) {
PyErr_BadInternalCall();
return NULL;
}
if (numfree) {
缓冲池可用
numfree--;
op = free_list[numfree];
_Py_NewReference((PyObject *)op);
count_reuse++;
} else {
缓冲池不可用
op = PyObject_GC_New(PyListObject, &PyList_Type);
if (op == NULL)
return NULL;
count_alloc++;
}
if (size <= 0)
op->ob_item = NULL;
else {
为列表申请内存空间
op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *));
if (op->ob_item == NULL) {
Py_DECREF(op);
return PyErr_NoMemory();
}
}
Py_SIZE(op) = size;
op->allocated = size;
_PyObject_GC_TRACK(op);
return (PyObject *) op;
}
从上面的创建动作我们可以看出,列表对象实际上分为两部分,一是PyListObject对象本身,二是PyListObject对象维护的元素列表,这是两块分离的内存,通过ob_item建立联系。
另外,创建新的PyListObject对象时我们可以看到非常熟悉的缓冲池技术。在创建PyListObject时,会先检查缓冲池free_list中是否有可用的对象。如果有,则直接使用,否则通过PyObject_GC_New咋系统堆中申请内存,创建新的PyListObject。
在python3.7.5中,默认情况下,free_list最多只有80个PyListObject对象。
#define PyList_MAXFREELIST 80
#endif
static PyListObject *free_list[PyList_MAXFREELIST];
static int numfree = 0;
2.2设置元素
假设我们创建一个包含6个元素的PyListObject,它的内存图如下:
注意创建一个list时列表元素不可能是null,这里只是为了演示元素变化。假设我们在第4个位置添加100,添加过程如下:
int
PyList_SetItem(PyObject *op, Py_ssize_t i,
PyObject *newitem)
{
PyObject **p;
if (!PyList_Check(op)) {
Py_XDECREF(newitem);
PyErr_BadInternalCall();
return -1;
}
【1】索引检查
if (i < 0 || i >= Py_SIZE(op)) {
Py_XDECREF(newitem);
PyErr_SetString(PyExc_IndexError,
"list assignment index out of range");
return -1;
}
【2】设置元素
p = ((PyListObject *)op) -> ob_item + i;
Py_XSETREF(*p, newitem);
return 0;
}
若索引正常,则【2】处将待加入的PyObject*指针放到有效位置,然后调整引用计数,将这个位置原来的对象引用计数-1.此时内存如下:
2.3 内存分配方式
在将元素添加,删除前需要讲讲PyListObject中的allocated的变化情况:
static int list_resize(PyListObject *self, Py_ssize_t newsize)
{
PyObject **items;
size_t new_allocated, num_allocated_bytes; #定义新分配内存的大小的变量
Py_ssize_t allocated = self->allocated; #已经分配的内存大小
【1】 当前分配内存必须比实际占用的内存要大,如果缩小(pop,del)
实际占用的内存大小小于分配内存大小的一半,则缩小内存
if (allocated >= newsize && newsize >= (allocated >> 1)) {
assert(self->ob_item != NULL || newsize == 0);
Py_SIZE(self) = newsize;
return 0;
}
【2】系统会分配足够的内存保证list不会出现溢出情况
内存的增长方式是: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
分配的最大值可能是 PY_SSIZE_T_MAX * (9 / 8) + 6 也就是说超预分配的量,大概只有总量的八分之一再加上3或者6
new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
【3】最大的分配值不可以超过系统定义的最大值
if (new_allocated > (size_t)PY_SSIZE_T_MAX / sizeof(PyObject *)) {
PyErr_NoMemory();
return -1;
}
【4】实际内存占用为0时将分配内存缩小到0
if (newsize == 0)
new_allocated = 0;
num_allocated_bytes = new_allocated * sizeof(PyObject *);
items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);
if (items == NULL) {
PyErr_NoMemory();
return -1;
}
self->ob_item = items;
Py_SIZE(self) = newsize;
self->allocated = new_allocated;
return 0;
}
从上面的算法我们可以看出,元素列表总内存变化算法为:new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6),也就是说超预分配空间是实际占用长度的1/8,如果ob_size小于9,则只需再加3,大于则再加6。
我们执行以下操作,元素列表内存变化情况如下:
l = list()
l.append(1)
l.append(2)
l.append(3)
l.append(4)
l.insert(1,5)
在要添加第一个元素时,根据增长模式,系统先分配4个空间,1,2,3,4元素都可以填充进去,在游标1 插入元素5时,系统判定内存不够,分配空间增加到8个空间,如图
然后进行
l.pop()
l.pop()
此时内存变化情况如下:
当pop到只剩下3个元素时,3 <(8/2),此时需要缩小内存,缩小办法:超预分配的内存是实际占用的1/8,如果《实际占用大小》< 9的话,再添加3个预分配空间,否则加6.
new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
2.4.常见操作原理:
1.插入操作:调用PyList_Insert函数,实际是调用内部的ins1函数
static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
Py_ssize_t i, n = Py_SIZE(self);
PyObject **items;
【1】如果插入的是空值则报错返回-1
if (v == NULL) {
PyErr_BadInternalCall();
return -1;
}
【2】如果自身内存已经达到系统定义最大值的话,则无法再添加
if (n == PY_SSIZE_T_MAX) {
PyErr_SetString(PyExc_OverflowError,
"cannot add more objects to list");
return -1;
}
【3】如果自身内存已经达到系统定义最大值的话,则无法再添加*/
if (list_resize(self, n+1) < 0)
return -1;
【4】如果游标为负数,表示倒数插入,比如-1表示插在倒数一个位置,-2表示倒数第二个*/
if (where < 0) {
where += n;
if (where < 0)
where = 0;
}
【5】如果游标为大于分配的空间尺寸,则插在倒数第一个*/
if (where > n)
where = n;
items = self->ob_item;
【5】插入位置右边元素向右移一位*/
for (i = n; --i >= where; )
items[i+1] = items[i];
Py_INCREF(v);
items[where] = v;
return 0;
}
int
PyList_Insert(PyObject *op, Py_ssize_t where, PyObject *newitem)
{
if (!PyList_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
return ins1((PyListObject *)op, where, newitem);
}
插入流程:
- 检查是否为空值;
- 检查allocated是否已经达到系统定义的最大值;
- 对负数游标进行转换;
- 对插入元素右边元素右移一位;
2.追加操作append:跟insert差不多,区别是不用移动右边元素,因为append是直接添加在末尾
3.切片slice操作:切片过程需要进行新建一个数组对象来保存切片数据,也就是说切片过程实质是一个深拷贝过程
static PyObject *
list_slice(PyListObject *a, Py_ssize_t ilow, Py_ssize_t ihigh)
{
ilow切片起始位置,ihigh结束位置
PyListObject *np;
PyObject **src, **dest;#src指向源切片的起始位置,dest数组指向保存数据的数组地址
Py_ssize_t i, len;
【1】起始位置情况判断
//起始小于0则默认0
if (ilow < 0)
ilow = 0;
//起始大于最大长度则默认最大长度作为起始
else if (ilow > Py_SIZE(a))
ilow = Py_SIZE(a);
if (ihigh < ilow)
ihigh = ilow;
else if (ihigh > Py_SIZE(a))
ihigh = Py_SIZE(a);
len = ihigh - ilow; //切片长度
np = (PyListObject *) PyList_New(len); //申请空间克隆
if (np == NULL)
return NULL;
src = a->ob_item + ilow; #源切片起始地址
dest = np->ob_item; 3目标存放数据的数组地址
【2】拷贝数据
for (i = 0; i < len; i++) {
PyObject *v = src[i];
Py_INCREF(v);
dest[i] = v;
}
return (PyObject *)np;
}
PyObject *
PyList_GetSlice(PyObject *a, Py_ssize_t ilow, Py_ssize_t ihigh)
{
if (!PyList_Check(a)) {
PyErr_BadInternalCall();
return NULL;
}
return list_slice((PyListObject *)a, ilow, ihigh);
}
4.列表拼接extend:拼接操作需要循环遍历2个列表且需新建一个数组对象来保存数据,其实质也是深拷贝。
static PyObject *
list_concat(PyListObject *a, PyObject *bb)
{
Py_ssize_t size;
Py_ssize_t i;
PyObject **src, **dest;
PyListObject *np;
if (!PyList_Check(bb)) {
PyErr_Format(PyExc_TypeError,
"can only concatenate list (not \"%.200s\") to list",
bb->ob_type->tp_name);
return NULL;
}
#define b ((PyListObject *)bb)
【1】拼接后长度不能大于系统定义的最大长度
if (Py_SIZE(a) > PY_SSIZE_T_MAX - Py_SIZE(b))
return PyErr_NoMemory();
size = Py_SIZE(a) + Py_SIZE(b);//拼接后的长度大小
np = (PyListObject *) PyList_New(size); //新建长度为size的数组对象
if (np == NULL) {
return NULL;
}
src = a->ob_item; //列表1的数据
dest = np->ob_item; //指向拼接后的列表数据地址
【2】 拷贝列表1数据
for (i = 0; i < Py_SIZE(a); i++) {
PyObject *v = src[i];
Py_INCREF(v);
dest[i] = v;
}
src = b->ob_item;//列表2的数据
dest = np->ob_item + Py_SIZE(a);//列表2数据在dest的插入点地址
【3】拷贝列表2数据
for (i = 0; i < Py_SIZE(b); i++) {
PyObject *v = src[i];
Py_INCREF(v);
dest[i] = v;
}
return (PyObject *)np;
#undef b
}
5.pop操作:可以看出pop某个位置(非末尾)的数据也需要循环遍历数组。
static PyObject *
list_pop_impl(PyListObject *self, Py_ssize_t index)
{
PyObject *v;
int status;
if (Py_SIZE(self) == 0) {
PyErr_SetString(PyExc_IndexError, "pop from empty list");
return NULL;
}
【1】pop位置小于0则表示从右边倒数第几pop,pop前提是不得超过实际长度
if (index < 0)
index += Py_SIZE(self);
if (index < 0 || index >= Py_SIZE(self)) {
PyErr_SetString(PyExc_IndexError, "pop index out of range");
return NULL;
}
v = self->ob_item[index];//需要pop的元素
【2】如果是最后一个元素,直接缩容
if (index == Py_SIZE(self) - 1) {
status = list_resize(self, Py_SIZE(self) - 1);
if (status >= 0)
return v; /* and v now owns the reference the list had */
else
return NULL;
}
Py_INCREF(v);
【3】进行切片合并
status = list_ass_slice(self, index, index+1, (PyObject *)NULL);
if (status < 0) {
Py_DECREF(v);
return NULL;
}
//返回删除的对象
return v;
}
另外需要说明的是,由于py里的列表维护了指针数组,元素可以是任意对象,所以每个元素大小不确定,这就导致了它不能像c数组一样连续排在内存里,,所以假若元素大小都一样,列表就有点吃亏了,它离散的对象位置不能很好的利用cpu高速缓存,造成了遍历需要更多的cpu周期。
list和tuple在c实现上是很相似的,都是一个指针数组,对于小对象来说,tuple会有一个对象池,所以小的、重复的使用tuple还有益处的。
3.PyListObject对象缓冲池
free_list中所缓冲的PyListObject是从哪里获取的,是何时创建的,答案就在PyListObject被销毁过程:
static void
list_dealloc(PyListObject *op)
{
Py_ssize_t i;
PyObject_GC_UnTrack(op);
Py_TRASHCAN_SAFE_BEGIN(op)
【1】销毁PyListObject维护的元素列表
if (op->ob_item != NULL) {
i = Py_SIZE(op);
while (--i >= 0) {
Py_XDECREF(op->ob_item[i]);
}
PyMem_FREE(op->ob_item);
}
【2】释放PyListObject自身
if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
free_list[numfree++] = op;
else
Py_TYPE(op)->tp_free((PyObject *)op);
Py_TRASHCAN_SAFE_END(op)
}
在创建一个新list时,我们可以看到创建过程分为两部,PyListObject对象和维护的元素列表。与之对应,在销毁一个list时,销毁过程也分离,先销毁PyListObject维护的元素列表,再释放PyListObject自身。
【1】处的工作没有特别之处,而在【2】出现了有趣的东西,在删除PyListObject自身时,python会先检查缓冲池free_list,查看其中缓冲的PyListObject对象是否满了,如果没有,就将该PyListObject对象放在缓冲池中,以备后用。
现在一切真相大白了,缓冲池被本应该死去的PyListObject对象给填充了,在以后创建新的PyListObject时,python会首先唤醒这些死去的PyListObject,又给他们一个生的机会。这里缓冲仅仅是PyListObject对象,不包括它曾经拥有的元素列表。