为什么 getsizeof 在 list() 和 [] 之间有不同的结果
Why is there a different result for getsizeof between list() and []
在工作的过程中,我发现了一件奇怪的事情:
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个字节的差异,我真的无法理解它们是从哪里来的。
这肯定与内部结构有关,但是有人可以向我解释一下幕后发生的事情吗?
TL;DR:列表过度分配,因此它们可以提供摊销的恒定时间 (O(1)
) 追加操作。过度分配的数量取决于列表的创建方式和实例的 append/delete 历史记录。列表文字总是事先知道大小并且不会过度分配(或只是轻微分配)。 list
函数 并不总是知道 结果的长度,因为它必须遍历参数,所以最终的过度分配取决于使用的 (取决于实现) 过度分配方案。
要了解我们正在查看的内容,重要的是要知道 sys.getsizeof
仅报告实例的大小。它不查看实例的内容。 因此内容的大小(在本例中为 int
s)未考虑在内。
实际影响列表大小的是(假定为 64 位系统):
- 8 字节:引用计数。
- 8 字节:指向 class.
的指针
- 8-bytes:存储列表中的元素个数(相当于
len(your_list)
)。
- 8 字节:存储包含列表中元素的数组的大小(这是
len(your_list) + over_allocation
)。
- 8 字节:指向存储内容指针的数组的指针。
列表的每个槽 8 字节:保存指向列表中每个元素的指针(或 NULL)。
24 字节:需要其他东西(我认为是垃圾收集)
这个解释可能有点难以理解,所以如果我添加一些图像(忽略用于垃圾收集的额外 24 个字节)可能会变得更清楚。我根据我在 Anaconda 的 CPython 3.7.2 Windows 64 位、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 列表是可变大小的,如果它只分配尽可能多的 space 来保存当前数量的项目,那么无论何时添加新项目,您都必须复制整个数组(使其成为 O(n)
)。但是,如果您过度分配,这意味着您实际占用的内存比存储元素所需的内存多,那么您可以支持分摊 O(1)
追加,因为它有时只需要调整大小。例如参见 [=37=].
下一点是文字总是知道它的大小,您将 x
项放入文字中,并且在源代码解析时它已经知道列表必须有多大。所以你可以简单地为这样的事情分配所需的内存:
l = [1, 2, 3]
然而,由于 list
是可调用的并且 Python 不会优化该调用,即使参数只是一个文字(我的意思是你可以为名称分配不同的东西 list
),它必须真的调用list
。
list
本身只是迭代参数并将项目附加到它的内部数组,在需要时调整大小并过度分配以使其摊销 O(1)
。 list
可以检查输入的大小,但由于(理论上)在迭代对象时任何事情都可能发生,因此将长度估计作为粗略的指导方针,而不是保证。因此,虽然它可以避免重新分配,但如果它可以预测参数中的项目数量,它仍然会过度分配(以防万一)。
请注意,所有这些都是 实现细节 ,它在其他 Python 实现中可能完全不同,即使在不同的 CPython 版本中也是如此。 Python 唯一保证的(我认为是,我不是 100% 确定)是 append
被摊销 O(1)
而不是它是如何实现的以及一个列表实例有多少内存需要。
在工作的过程中,我发现了一件奇怪的事情:
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个字节的差异,我真的无法理解它们是从哪里来的。
这肯定与内部结构有关,但是有人可以向我解释一下幕后发生的事情吗?
TL;DR:列表过度分配,因此它们可以提供摊销的恒定时间 (O(1)
) 追加操作。过度分配的数量取决于列表的创建方式和实例的 append/delete 历史记录。列表文字总是事先知道大小并且不会过度分配(或只是轻微分配)。 list
函数 并不总是知道 结果的长度,因为它必须遍历参数,所以最终的过度分配取决于使用的 (取决于实现) 过度分配方案。
要了解我们正在查看的内容,重要的是要知道 sys.getsizeof
仅报告实例的大小。它不查看实例的内容。 因此内容的大小(在本例中为 int
s)未考虑在内。
实际影响列表大小的是(假定为 64 位系统):
- 8 字节:引用计数。
- 8 字节:指向 class. 的指针
- 8-bytes:存储列表中的元素个数(相当于
len(your_list)
)。 - 8 字节:存储包含列表中元素的数组的大小(这是
len(your_list) + over_allocation
)。 - 8 字节:指向存储内容指针的数组的指针。
列表的每个槽 8 字节:保存指向列表中每个元素的指针(或 NULL)。
24 字节:需要其他东西(我认为是垃圾收集)
这个解释可能有点难以理解,所以如果我添加一些图像(忽略用于垃圾收集的额外 24 个字节)可能会变得更清楚。我根据我在 Anaconda 的 CPython 3.7.2 Windows 64 位、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 列表是可变大小的,如果它只分配尽可能多的 space 来保存当前数量的项目,那么无论何时添加新项目,您都必须复制整个数组(使其成为 O(n)
)。但是,如果您过度分配,这意味着您实际占用的内存比存储元素所需的内存多,那么您可以支持分摊 O(1)
追加,因为它有时只需要调整大小。例如参见 [=37=].
下一点是文字总是知道它的大小,您将 x
项放入文字中,并且在源代码解析时它已经知道列表必须有多大。所以你可以简单地为这样的事情分配所需的内存:
l = [1, 2, 3]
然而,由于 list
是可调用的并且 Python 不会优化该调用,即使参数只是一个文字(我的意思是你可以为名称分配不同的东西 list
),它必须真的调用list
。
list
本身只是迭代参数并将项目附加到它的内部数组,在需要时调整大小并过度分配以使其摊销 O(1)
。 list
可以检查输入的大小,但由于(理论上)在迭代对象时任何事情都可能发生,因此将长度估计作为粗略的指导方针,而不是保证。因此,虽然它可以避免重新分配,但如果它可以预测参数中的项目数量,它仍然会过度分配(以防万一)。
请注意,所有这些都是 实现细节 ,它在其他 Python 实现中可能完全不同,即使在不同的 CPython 版本中也是如此。 Python 唯一保证的(我认为是,我不是 100% 确定)是 append
被摊销 O(1)
而不是它是如何实现的以及一个列表实例有多少内存需要。