为什么两个相同的列表有不同的内存占用?
Why do two identical lists have a different memory footprint?
我创建了两个列表 l1
和 l2
,但每个列表都有不同的创建方法:
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 内部的东西,还是其他一些解释?
当您编写 [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
操作码。来自 docs:
LIST_APPEND(i)
Calls list.append(TOS[-i], TOS)
. Used to implement list comprehensions.
现在,对于列表重复操作,如果我们考虑:
>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144
所以,它似乎能够准确地分配大小。查看 source code,我们看到这正是发生的事情:
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;
。其余函数只是简单地填充数组。
None是一块内存,但不是预先指定的大小。除此之外,数组元素之间的数组中还有一些额外的间距。您可以通过 运行:
自己查看
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] 的大小永远不能大于变量设置为存储的可用相邻内存,如果稍后动态分配更大的变量,则可能必须移动变量。
我创建了两个列表 l1
和 l2
,但每个列表都有不同的创建方法:
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 内部的东西,还是其他一些解释?
当您编写 [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
操作码。来自 docs:
LIST_APPEND(i)
Calls
list.append(TOS[-i], TOS)
. Used to implement list comprehensions.
现在,对于列表重复操作,如果我们考虑:
>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144
所以,它似乎能够准确地分配大小。查看 source code,我们看到这正是发生的事情:
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;
。其余函数只是简单地填充数组。
None是一块内存,但不是预先指定的大小。除此之外,数组元素之间的数组中还有一些额外的间距。您可以通过 运行:
自己查看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] 的大小永远不能大于变量设置为存储的可用相邻内存,如果稍后动态分配更大的变量,则可能必须移动变量。