为什么两个相同的列表有不同的内存占用?

我创建了两个列表 l1l2,但每一个都有不同的创建方法:

import sys


l1 = [None] * 10
l2 = [None for _ in range(10)]


print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))

但结果让我吃惊:

Size of l1 = 144
Size of l2 = 192

使用列表内涵创建的列表在内存中的大小更大,但是在 Python 中这两个列表是相同的。

为什么呢? 这是某种 CPython 内部的东西,或其他解释?

7867 次浏览

当您编写 [None] * 10时,Python 知道它需要一个恰好包含10个对象的列表,因此它会精确地分配这些对象。

当你使用一个列表内涵的时候,Python 不知道它需要多少。因此,随着元素的添加,列表逐渐增大。对于每个重新分配,它分配的空间超过了立即需要的空间,因此它不必为每个元素重新分配。由此产生的列表可能会比需要的多一些。

当比较创建的大小相似的列表时,您可以看到这种行为:

>>> sys.getsizeof([None]*15)
184
>>> sys.getsizeof([None]*16)
192
>>> sys.getsizeof([None for _ in range(15)])
192
>>> sys.getsizeof([None for _ in range(16)])
192
>>> sys.getsizeof([None for _ in range(17)])
264

您可以看到,第一个方法只分配所需的内容,而第二个方法会周期性地增长。在这个例子中,它分配了足够16个元素,并且在到达第17个元素时不得不重新分配。

正如在 这个问题中指出的那样,列表理解在底层使用 list.append,因此它将调用 list-resize 方法,该方法重分配。

为了向自己演示这一点,实际上可以使用 dis反汇编程序:

>>> code = compile('[x for x in iterable]', '', 'eval')
>>> import dis
>>> dis.dis(code)
1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10560b810, file "", line 1>)
2 LOAD_CONST               1 ('<listcomp>')
4 MAKE_FUNCTION            0
6 LOAD_NAME                0 (iterable)
8 GET_ITER
10 CALL_FUNCTION            1
12 RETURN_VALUE


Disassembly of <code object <listcomp> at 0x10560b810, file "", line 1>:
1           0 BUILD_LIST               0
2 LOAD_FAST                0 (.0)
>>    4 FOR_ITER                 8 (to 14)
6 STORE_FAST               1 (x)
8 LOAD_FAST                1 (x)
10 LIST_APPEND              2
12 JUMP_ABSOLUTE            4
>>   14 RETURN_VALUE
>>>

注意 <listcomp>代码对象反汇编中的 LIST_APPEND操作码:

LIST _ APPEND (i)

调用 list.append(TOS[-i], TOS)。用于实现列表理解。

现在,对于列表重复操作,我们有一个关于正在发生什么的提示,如果我们考虑:

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144

因此,它似乎能够 没错分配的大小。看看 源代码,我们看到的正是这种情况:

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
Py_ssize_t i, j;
Py_ssize_t size;
PyListObject *np;
PyObject **p, **items;
PyObject *elem;
if (n < 0)
n = 0;
if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
return PyErr_NoMemory();
size = Py_SIZE(a) * n;
if (size == 0)
return PyList_New(0);
np = (PyListObject *) PyList_New(size);

也就是说,这里: size = Py_SIZE(a) * n;。其余的函数只是填充数组。

没有一个是内存块,但它不是预先指定的大小。除此之外,数组元素之间还有一些额外的间距。你可以通过跑步来看到这一点:

for ele in l2:
print(sys.getsizeof(ele))


>>>>16
16
16
16
16
16
16
16
16
16

它不是 l2的总和,而是更小。

print(sys.getsizeof([None]))
72

这比 l1的十分之一大得多。

您的编号应该根据操作系统的详细信息和操作系统中当前内存使用情况的详细信息而有所不同。[ None ]的大小永远不能大于设置变量存储的可用相邻内存,并且如果以后动态地分配变量以便更大,则可能必须移动变量。