我刚试验了内存中python数据结构的大小。我写了以下片段:
import sys
lst1=[]
lst1.append(1)
lst2=[1]
print(sys.getsizeof(lst1), sys.getsizeof(lst2))
我在以下配置上测试了代码:
52 40
所以lst1有52个字节,lst2有40个字节。48 32
48 36
任何人都可以向我解释为什么这两种尺寸不同,尽管两种尺寸都是包含1?
的列表在getsizeof函数的python文档中,我发现了以下内容:...adds an additional garbage collector overhead if the object is managed by the garbage collector.
在我的小例子中可能就是这种情况吗?
答案 0 :(得分:105)
这是一个更全面的交互式会话,可以帮助我解释发生了什么(Windows XP 32位上的Python 2.6,但实际上并不重要):
>>> import sys
>>> sys.getsizeof([])
36
>>> sys.getsizeof([1])
40
>>> lst = []
>>> lst.append(1)
>>> sys.getsizeof(lst)
52
>>>
请注意,空列表略小于其中[1]
的列表。但是,当附加一个元素时,它会变得更大。
原因是CPython源代码中Objects/listobject.c
的实现细节。
当创建空列表[]
时,不会分配任何元素空间 - 这可以在PyList_New
中看到。 36字节是32位计算机上列表数据结构本身所需的空间量。
当创建具有单个元素[1]
的列表时,除了列表数据结构本身所需的内存之外,还分配一个元素的空间。同样,这可以在PyList_New
中找到。给定size
作为参数,它计算:
nbytes = size * sizeof(PyObject *);
然后有:
if (size <= 0)
op->ob_item = NULL;
else {
op->ob_item = (PyObject **) PyMem_MALLOC(nbytes);
if (op->ob_item == NULL) {
Py_DECREF(op);
return PyErr_NoMemory();
}
memset(op->ob_item, 0, nbytes);
}
Py_SIZE(op) = size;
op->allocated = size;
所以我们看到size = 1
,分配了一个指针的空间。 4个字节(在我的32位盒子上)。
在空列表上调用append
时,会发生以下情况:
PyList_Append
来电app1
app1
要求列表的大小(并将0作为答案)app1
然后使用list_resize
(在我们的例子中为1)调用size+1
list_resize
有一个有趣的分配策略,在其来源的评论中进行了总结。这是:
/* This over-allocates proportional to the list size, making room
* for additional growth. The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
*/
new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);
/* check for integer overflow */
if (new_allocated > PY_SIZE_MAX - newsize) {
PyErr_NoMemory();
return -1;
} else {
new_allocated += newsize;
}
让我们看看我在文章开头的会话中引用的数字是如何达到的。
因此,36字节是列表数据结构本身在32位上所需的大小。使用单个元素,为一个指针分配空间,因此4个额外字节 - 总共40个字节。好了到目前为止。
在空列表中调用app1
时,它会使用list_resize
调用size=1
。根据{{1}}的过度分配算法,1之后的下一个最大可用大小为4,因此将分配4个指针的位置。 4 * 4 = 16字节,36 + 16 = 52。
确实,一切都有道理: - )
答案 1 :(得分:9)
正在发生的事情是你正在研究如何分配列表(我想也许你只是想看看有多大的东西 - 在这种情况下,使用sys.getsizeof()
)
当某些内容添加到列表中时,可能会发生以下两种情况之一:
额外项目适合备用空间
需要额外的空间,因此会创建一个新列表,并复制内容,并添加额外的内容。
因为(2)是昂贵的(复制东西,甚至是指针,花费的时间与要复制的东西的数量成正比,所以随着列表变大而增长)我们希望不经常这样做。所以我们不是只添加更多的空间,而是添加一大块空间。通常,添加量的大小与已经使用的大小类似 - 数学计算得出分配内存的平均成本(分散在许多用途中)仅与列表大小成比例。
所以你所看到的与这种行为有关。我不知道确切的细节,但如果[]
或[1]
(或两者)都是特殊情况,我将不会感到惊讶,只有足够的内存被分配(在这些常见情况下节省内存) ),然后附加上面描述的“抓住一个新块”,增加更多。
但我不知道确切的细节 - 这就是动态数组的工作原理。 python中列表的确切实现将进行微调,以便它适用于典型的python程序。所以我真正说的是你不能相信列表的大小来确切地告诉你它包含多少 - 它可能包含额外的空间,并且额外的可用空间量很难判断或预测。
ps一个简洁的替代方法是将列表设为(value, pointer)
对,其中每个指针指向下一个元组。通过这种方式,您可以逐步增加列表,尽管使用的总内存更高。这是一个链表(python使用的更像是矢量或动态数组)。
[更新]见Eli的优秀答案。他/她解释说,[]
和[1]
都是完全分配的,但是附加到[]
的分配了一个额外的块。代码中的注释就是我上面所说的(这称为“过度分配”,数量与我们所拥有的相对应,因此平均(“摊销”)成本与大小成正比)。
答案 2 :(得分:3)
以下是列表增长模式的快速演示。更改range()中的第三个参数将更改输出,使其看起来不像listobject.c中的注释,但只是附加一个元素时的结果似乎完全准确。
allocated = 0
for newsize in range(0,100,1):
if (allocated < newsize):
new_allocated = (newsize >> 3) + (3 if newsize < 9 else 6)
allocated = newsize + new_allocated;
print newsize, allocated
答案 3 :(得分:1)
基于系统架构的公式更改 (size-36)/ 4(适用于32位计算机)和 (size-64)/ 8(用于64位计算机)
36,64-基于计算机的空列表的大小 4,8-基于计算机的列表中单个元素的大小