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

Posted

技术标签:

【中文标题】为啥两个相同的列表有不同的内存占用?【英文标题】:Why do two identical lists have a different memory footprint?为什么两个相同的列表有不同的内存占用? 【发布时间】:2019-01-02 16:57:22 【问题描述】:

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

【问题讨论】:

可能,重复操作符会调用一些函数来精确地调整底层数组的大小。请注意,144 == sys.getsizeof([]) + 8*10) 其中 8 是指针的大小。 请注意,如果将10 更改为11[None] * 11 列表的大小为152,但列表解析的大小仍然为192。之前链接的问题不是完全重复的,但它有助于理解为什么会发生这种情况。 【参考方案1】:

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

【讨论】:

是的,这是有道理的。当我知道前面的大小时,使用* 创建列表可能会更好。 @AndrejKesely 在您的列表中仅使用 [x] * n 和不可变的 x。结果列表将包含对相同对象的引用。 @schwobaseggl 好吧,这可能是你想要的,但最好理解这一点。 @juanpa.arrivillaga 确实,它可能是。但通常情况并非如此,特别是 SO 到处都是张贴者想知道为什么他们的所有数据同时更改:D【参考方案2】:

如this question 中所述,list-comprehension 在后台使用list.append,因此它将调用 list-resize 方法,该方法会过度分配。

为了向自己演示这一点,您实际上可以使用dis dissasembler:

>>> 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
>>>

注意&lt;listcomp&gt; 代码对象的反汇编中的LIST_APPEND 操作码。来自docs:

LIST_APPEND(i)

致电list.append(TOS[-i], TOS)。用于实现列表推导。

现在,对于 list-repetition 操作,如果我们考虑一下,我们就会知道发生了什么:

>>> 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;。其余的函数只是简单地填充数组。

【讨论】:

“正如在这个问题中指出的,列表理解在后台使用 list.append”我认为说它使用.extend() 更准确。 @Accccumulation 你为什么这么认为? 因为它不是一个接一个地附加元素。当您将元素附加到列表时,您实际上是在创建一个具有新内存分配的新列表,并将列表放入新的内存分配中。另一方面,列表推导式将大部分新元素放入已分配的内存中,当它们用完分配的内存时,它们会分配另一块内存,这对于新元素来说是不够的。 @Accumulation 这是不正确的。 list.append 是一个摊销的常数时间操作,因为当列表调整大小时,它会过度分配。因此,并非每个附加操作都会导致新分配的数组。无论如何,我链接到的问题在源代码中向您显示,事实上,列表推导 do 使用 list.append,。我稍后会回到我的笔记本电脑,我可以向您展示反汇编的字节码以进行列表理解和相应的LIST_APPEND操作码【参考方案3】:

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

【讨论】:

None 实际上并未存储在底层数组中,唯一存储的是PyObject 指针(8 个字节)。所有 Python 对象都在堆上分配。 None 是一个单例,因此拥有一个包含许多无的列表只会在堆上创建指向同一 None 对象的 PyObject 指针数组(并且不会在每个附加 None 的过程中使用额外的内存)。我不确定您所说的“没有预先指定的大小”是什么意思,但这听起来不正确。最后,带有getsizeof 每个元素的循环并没有展示您认为它正在展示的内容。 如果你说的是真的,那么[None]*10的大小应该和[None]的大小一样。但显然情况并非如此——增加了一些额外的存储空间。事实上,[None] 的大小重复十次(160)也小于 [None] 的大小乘以 10。正如您所指出的,显然指向 [None] 的指针的大小小于 [None] 本身的大小(16 个字节而不是 72 个字节)。但是,160+32 是 192。我认为前面的答案也不能完全解决问题。很明显,分配了一些额外的少量内存(可能取决于机器状态)。 “如果你说的是真的,那么 [None]*10 的大小应该与 [None] 的大小相同”我在说什么可能暗示这点?同样,您似乎专注于底层缓冲区被过度分配的事实,或者列表的大小包括的大小超过了底层缓冲区的大小(当然是这样),但这不是重点这个问题。同样,您在 l2 的每个 ele 上使用 gestsizeof 具有误导性,因为 getsizeof(l2) 没有考虑容器内元素的大小 要向自己证明最后一个声明,请执行l1 = [None]; l2 = [None]*100; l3 = [l2] 然后print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3))。你会得到如下结果:72 864 72。也就是说,分别是64 + 1*864 + 100*864 + 1*8,再次假设一个具有 8 字节指针大小的 64 位系统。 正如我所说,sys.getsizeof *不考虑容器中项目的大小。来自docs:“仅考虑直接归因于对象的内存消耗,而不考虑它所指的对象的内存消耗......请参阅recursive sizeof 配方以获取递归使用 getsizeof() 来查找大小的示例容器及其所有内容。”

以上是关于为啥两个相同的列表有不同的内存占用?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个函数会占用大量内存?

为啥JAVA进程占用内存会超过Xmx设置

linux和winxp,chromium为啥linux占用内存大

为啥我的 UIImage 占用这么多内存?

与 CGContextDrawImage 相比,为啥 UIImageView 如此占用内存

如何正确统计C程序运行的内存占用量?