【问题标题】:Why is there a different result for getsizeof between list() and [] [duplicate]为什么 list() 和 [] 之间的 getsizeof 有不同的结果 [重复]
【发布时间】:2019-02-21 13:08:39
【问题描述】:

在工作的时候,我注意到一件奇怪的事情:

from sys import getsizeof as gs

list1=[1]
list2=list([1])

list1==list2 #true
gs(list1)    #80.  (I guess 72 overhead +8 of the int)
gs(list2)    #104. (I guess 72 + 8 as above + 24 of...?)


list3=[1,2,3,4,5]
list4=list(list3)

gs(list3)    #112
gs(list4)    #136

所以总是有这 24 个字节的差异,我无法真正理解它们的来源。

这肯定与内部有关,但谁能解释一下幕后发生了什么?

【问题讨论】:

  • 听起来类似于我前段时间问的一个问题:stackoverflow.com/a/54445004/4349415 - 虽然我问的是副本的大小,而不是构造函数的差异,但在引擎盖下它可能是相关的。跨度>
  • 因为这是探测实现细节,如果你添加你的 python 实现和使用的 Python 版本作为标签,它会很有用(特别是对于验证)。例如如果您使用的是 CPython 3.7,则为 CPython 和 Python-3.7。

标签: python list memory-management python-internals


【解决方案1】:

TL;DR:列表过度分配,因此它们可以提供摊销的常量时间 (O(1)) 追加操作。过度分配的数量取决于列表的创建方式和实例的追加/删除历史。列表文字总是事先知道大小,并且不会过度分配(或只是轻微地)。 list 函数并不总是知道结果的长度,因为它必须遍历参数,所以最终的过度分配取决于使用的(依赖于实现 ) 过度分配方案。

要了解我们正在查看的内容,重要的是要知道sys.getsizeof 仅报告实例的大小。它不查看实例的内容。 因此不考虑内容的大小(在本例中为 ints)。

实际上影响列表大小的是(假设为 64 位系统):

  • 8 字节:引用计数。
  • 8 字节:指向类的指针。
  • 8-bytes:存储列表中元素的个数(相当于len(your_list))。
  • 8 字节:存储包含列表中元素的数组的大小(这是len(your_list) + over_allocation)。
  • 8 字节:指向存储指向内容的指针的数组的指针。
  • 列表中每个槽 8 字节:保存指向列表中每个元素的指针(或 NULL)。

  • 24 字节:其他东西需要(我认为垃圾收集)

这个解释可能有点难以理解,所以如果我添加一些图像可能会变得更清楚(忽略用于垃圾收集的额外 24 个字节)。我根据我在 CPython 3.7.2 Windows 64 位、Anaconda 的 Python 64 位上的发现创建了它们。

没有过度分配,例如对于mylist = [1,2,3]

过度分配,例如对于mylist = list([1,2,3])

或者手动appends:

mylist = []
mylist.append(1)
mylist.append(2)
mylist.append(3)

这意味着一个空列表已经占用了 64 个字节,假设空列表没有过度分配。对于添加的每个元素,必须添加对 Python 对象的另一个引用(指针为 8 个字节)。

所以list 的最小尺寸是:

size_min = 64 + 8 * n_items

Python 列表是可变大小的,如果它只分配尽可能多的空间来保存当前数量的项目,那么每当添加新项目时,您都必须复制整个数组(使其成为 O(n))。但是,如果您过度分配,这意味着您实际上占用的内存比存储元素所需的更多,那么您可以支持摊销 O(1) 追加,因为它有时只需要调整大小。参见例如Wikipedia "Amortized analysis"

下一点是文字总是知道它的大小,您将x 项放在文字中,并且在源代码解析时已经知道列表必须有多大。所以你可以简单地为这样的事情分配所需的内存:

l = [1, 2, 3]

但是,由于list 是可调用的,并且 Python 不会优化该调用,即使参数只是一个文字(我的意思是你可以为名称 list 分配不同的东西),它必须 真的 打电话给list

list 本身只是迭代参数并将项目附加到它的内部数组,在需要时调整大小并过度分配以使其摊销O(1)list 可以检查输入的大小,但由于(理论上)在迭代对象时可能发生任何事情,因此将长度估计作为粗略的指导,而不是作为保证。因此,如果它可以预测参数中的项目数,它会避免重新分配,但它仍然会过度分配(以防万一)。

请注意,所有这些都是实现细节,在其他 Python 实现中可能完全不同,即使在不同的 CPython 版本中也是如此。 Python 唯一可以保证的(我认为可以,但我不是 100% 确定)是 append 摊销了 O(1),而不是如何实现以及列表实例需要多少内存。

【讨论】:

  • 我会注意到 list 不能(或不)迭代其参数以确定要分配多少元素有点奇怪,因为 str.join 做了类似的事情。我猜这是因为list.__new__ 实际上并没有看它的论点;它只是分配一个空列表并将参数传递给list.__init__ 以实际填充新列表。
  • @chepner 是的,我写的并不是所有版本的 Python 都正确。我忽略了一些细节,例如在 CPython 3.7.2 list.extend(由 list.__init__ 调用)检查它刚刚过度分配 x 元素的大小(其中 x depends on the number of elements)。但与上一个版本相比,这种情况发生了几次变化。
  • @chepner 我认为这里的主要问题是,即使您在开始迭代时知道参数的长度,也不能保证在迭代期间长度不会改变,因此“悲观”方法是分配“可见长度+一点”,至少它避免了所有调整到该长度的大小。并且没有过度分配也很棘手(至少在查看性能时),因为此后的第一个附加将是O(n)(这对于不可变且可能不会过度分配的字符串是不同的,所以@987654362 @知道确切的大小)。
  • @MSeifert 感谢您的解释,“但是,由于 list 是可调用的,Python 不会优化该调用,即使参数只是一个文字 (...),它必须真的来电清单。”真的让我大开眼界!
猜你喜欢
  • 2021-03-20
  • 1970-01-01
  • 2017-04-06
  • 2021-10-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-12
  • 2020-09-03
  • 1970-01-01
相关资源
最近更新 更多