关于python:为什么从列表中删除项目时内存大小没有变化?

Why doesn't the memory size change when you delete an item from the list?

是的,为什么从列表中删除项目时大小不改变?有没有办法改变这种行为?

1
2
3
4
5
6
7
8
Python 2.7.5+ (default, Feb 27 2014, 19:37:08)
>>> from sys import getsizeof
>>> x = [1, 2, 3, 4]
>>> print getsizeof(x), x
104 [1, 2, 3, 4]
>>> del x[3]
>>> print getsizeof(x), x
104 [1, 2, 3]


当你添加一个项目时,它通常也不会改变。这是因为list的实现过度分配为优化。除了修改python的源代码之外,没有其他方法可以更改这个。


getsizeof得到列表对象的内存消耗量,而不是它的长度或类似的东西。删除项目不会导致列表释放内存,除非删除超过某个阈值;它会保留该内存以保存将来的项目,从而降低分配成本。

另外,getsizeof不包括列表元素占用的内存,只包括列表头和指针动态数组的内存。

如果要减少内存消耗,请创建列表的切片副本:

1
2
3
4
5
6
>>> x = [1]*3
>>> del x[2]
>>> sys.getsizeof(x)
88
>>> sys.getsizeof(x[:])
80

不过,这通常是不必要的,而且对每个删除都这样做几乎肯定是一个坏主意。


根据listobject.c源代码,list的大小调整遵循一定的模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
/* Bypass realloc() when a previous overallocation is large enough
   to accommodate the newsize.  If the newsize falls lower than half
   the allocated size, then proceed with the realloc() to shrink the list.
*/
if (allocated >= newsize && newsize >= (allocated >> 1)) {
    assert(self->ob_item != NULL || newsize == 0);
    Py_SIZE(self) = newsize;
    return 0;
}

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 */
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
...

仅因为附加或删除了某个项而不可变地调整大小将导致性能不佳。


列表对象不会在每次删除时调整内部C数组的大小;这会非常低效。

当添加元素时,列表对象会根据需要定期过度分配新内存;当删除时,仅当删除的内容足以容纳已分配的一半空间时,才会调整大小。从C代码:

1
2
3
4
5
if (allocated >= newsize && newsize >= (allocated >> 1)) {
    assert(self->ob_item != NULL || newsize == 0);
    Py_SIZE(self) = newsize;
    return 0;
}

其中newsize是存储的对象引用的实际计数,allocated是过度分配的列表对象的大小。当newsize仍大于或等于分配空间的一半时,上述测试跳过重新分配。

即使在缩小列表时,数组仍会被分配过多以接收新元素;列表数组中始终至少有3个插槽是空的。

因此,在删除足够的元素之前,sys.getsizeof()保持稳定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> from sys import getsizeof
>>> x = [1, 2, 3, 4] * 3
>>> x
[1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]
>>> print getsizeof(x), x
168 [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]
>>> del x[-1]
>>> del x[-1]
>>> del x[-1]
>>> del x[-1]
>>> print getsizeof(x), x
168 [1, 2, 3, 4, 1, 2, 3, 4]
>>> del x[-1]
>>> del x[-1]
>>> del x[-1]
>>> del x[-1]
>>> print getsizeof(x), x
136 [1, 2, 3, 4]

在另一个方向上,当添加元素时,列表过度分配会按存储的引用数的大小的比例逐步增加列表:

1
2
3
4
5
6
7
8
/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 */
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

参见listobject.c源代码(python 2.7版本)的list_resize()函数。

当然,实际的len()输出确实反映了列表引用的项目数。


最好的猜测是,每次从列表中删除一个项目时,python都不会释放列表的末尾。它会在这个空间上停留一段时间,以防再次需要它。它可能会等到有足够多的空位来保证在一次大的突袭中清除它们。