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


155

我创建了两个列表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内部的东西,还是其他解释?


2
可能,重复运算符将调用某些函数,该函数将精确调整基础数组的大小。注意,144 == sys.getsizeof([]) + 8*10)其中8是指针的大小。
juanpa.arrivillaga

1
请注意,如果更改1011[None] * 11列表将具有大小152,但是列表理解仍将具有大小192。先前链接的问题并非完全相同,但对于理解为什么会发生这一问题很重要。
Patrick Haugh

Answers:


162

当您编写时[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个元素时不得不重新分配。


1
是的,那很有道理。*当我知道前面的大小时,最好使用它来创建列表。
Andrej Kesely '18

27
@AndrejKesely仅在您的列表中使用[x] * n不可变x。结果列表将包含对相同对象的引用。
schwobaseggl

5
@schwobaseggl很好,这可能就是您想要的,但是很高兴了解这一点。
juanpa.arrivillaga

19
@ juanpa.arrivillaga确实如此。但通常情况并非如此,特别是SO到处都是海报,他们想知道为什么所有数据都同时更改:D
schwobaseggl

50

就像在这个问题中指出的那样,列表理解是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
>>>

注意LIST_APPEND反汇编<listcomp>代码对象中的操作码。从文档

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;。其余函数仅填充数组。


“正如在这个问题中所指出的那样,list-comprehension使用list.append在幕后”,我认为说它使用更准确.extend()
累积

@积累为什么这么相信?
juanpa.arrivillaga

因为它不是一个接一个地添加元素。将元素追加到列表时,实际上是在创建具有新内存分配的新列表,并将该列表放入该新内存分配中。另一方面,列表推导将大多数新元素放入已分配的内存中,当它们用完分配的内存时,就会分配另一个内存,而不仅仅是新元素。
累积

7
@Acccumulation不正确。list.append是分期摊销的固定时间操作,因为当列表调整大小时,它会重新分配。因此,并非每个追加操作都会导致新分配一个数组。无论如何,我链接到的问题实际上在源代码中向您显示了列表推导确实使用了list.append.。我待会儿会回到我的笔记本电脑上,我可以向您展示用于列表理解的反汇编字节码和相应的LIST_APPEND操作码
juanpa.arrivillaga 18/07/26

3

没有一个是内存块,但是它不是预先指定的大小。除此之外,数组元素之间的数组还有一些额外的间距。您可以通过运行以下命令自己查看:

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]的大小永远不能大于将变量设置为存储的可用相邻内存,并且如果以后将其动态分配为更大,则可能必须移动该变量。


1
None实际上并没有存储在底层数组中,唯一存储的是PyObject指针(8个字节)。所有Python对象均分配在堆上。None是一个单例,因此拥有一个无很多的列表将简单地创建一个指向None堆上同一对象的PyObject指针数组(并且不会在进程中额外使用任何内存None)。我不确定您的意思是“没有没有预先指定的大小”,但这听起来不正确。最后,您与getsizeof每个元素的循环并没有证明您似乎认为这是在证明。
juanpa.arrivillaga

如果您说的是正确的,则[None] * 10的大小应与[None]的大小相同。但显然不是这样-已添加了一些额外的存储空间。实际上,[无]的大小重复十次(160)也小于[无]的大小乘以十。如您所指出的,指向[None]的指针的大小显然小于[None]本身的大小(16个字节而不是72个字节)。但是,160 + 32是192。我也不认为前面的答案也可以完全解决问题。显然,分配了一些额外的少量内存(可能取决于计算机状态)。
StevenJD '18年

“如果您说的是真的,[None] * 10的大小应与[None]的大小相同”,这是什么意思?同样,您似乎集中在以下事实:基础缓冲区被过度分配,或者列表的大小包含的内容超过了基础缓冲区的大小(当然是这样),但这并不是重点。这个问题。再次,您gestsizeof对每种方法ele的使用都具有l2误导性,因为getsizeof(l2) 它没有考虑容器内元素的大小
juanpa.arrivillaga

为了证明自己是最后一个要求,做l1 = [None]; l2 = [None]*100; l3 = [l2]print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3))。您会得到类似的结果:72 864 72。即,分别为64 + 1*864 + 100*8,和64 + 1*8,再次,假定有8个字节的指针尺寸的64位系统。
juanpa.arrivillaga

1
正如我已经说过的,sys.getsizeof*不考虑容器中项目的大小。从文档中:“仅考虑直接归因于该对象的内存消耗,而不考虑它所引用的对象的内存消耗...有关递归使用getsizeof()来查找容器大小的示例,请参见递归sizeof配方所有内容。”
juanpa.arrivillaga
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.