内存中 Python 列表的大小[重复]
- 2025-02-21 08:48:00
- admin 原创
- 23
问题描述:
我刚刚试验了内存中 Python 数据结构的大小。我写了以下代码片段:
import sys
lst1=[]
lst1.append(1)
lst2=[1]
print(sys.getsizeof(lst1), sys.getsizeof(lst2))
我在以下配置上获得了以下输出:
Windows 7 64位,Python3.1:(
52 40
因此lst1
有52个字节并且lst2
有40个字节)Ubuntu 11.4 32位,带有Python3.2:输出为
48 32
Ubuntu 11.4 32位 Python2.7:
48 36
有人能向我解释为什么虽然都是包含 1 的列表,但两个列表的大小却不同吗?
在该函数的 Python 文档中getsizeof
我发现了以下内容:
...如果对象由垃圾收集器管理,则会增加额外的垃圾收集器开销。
在我的这个小例子中可能存在这种情况吗?
解决方案 1:
这里有一个更完整的交互式会话,它将帮助我解释发生了什么(Windows XP 32 位上的 Python 2.6,但这并不重要):
>>> import sys
>>> sys.getsizeof([])
36
>>> sys.getsizeof([1])
40
>>> lst = []
>>> lst.append(1)
>>> sys.getsizeof(lst)
52
>>>
注意,空列表比包含元素的列表小一点[1]
。但是,当添加元素时,它会变得大得多。
造成这种情况的原因在于Objects/listobject.c
CPython 源代码中的实现细节。
空列表
当创建一个空列表时[]
,没有为元素分配任何空间 - 这可以在中看到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
(size+1
在我们的例子中是 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
。根据 的过度分配算法list_resize
,1 之后的下一个最大可用大小是 4,因此将分配 4 个指针的位置。4 * 4 = 16 字节,而 36 + 16 = 52。
确实,一切都有道理:-)
解决方案 2:
您正在查看列表是如何分配的(我想也许您只是想看看事情有多大 - 在这种情况下,使用sys.getsizeof()
)
当某个东西被添加到列表中时,可能会发生以下两种情况之一:
多余的物品可放入空余空间。
需要额外的空间,因此需要创建一个新列表,并将内容复制过去,并添加额外的内容。
由于 (2) 的成本很高(复制内容,即使是指针,所需的时间也与要复制的内容数量成正比,因此随着列表变大,复制时间也会增加),我们希望不经常这样做。因此,我们不会只添加一点空间,而是添加整个块。通常,添加的数量与已在使用的数量相似 - 这样,数学计算得出,分配内存的平均成本(分摊到多次使用中)仅与列表大小成正比。
所以您看到的情况与此行为有关。我不知道具体细节,但如果[]
或[1]
(或两者)是特殊情况,我不会感到惊讶,在这种情况下只分配了足够的内存(以在这些常见情况下节省内存),然后附加操作会执行上述“获取新块”的操作,从而添加更多内存。
但我不知道具体细节——这只是动态数组的一般工作方式。Python 中列表的确切实现将经过精细调整,以使其最适合典型的 Python 程序。所以我真正想说的是,您不能相信列表的大小会准确地告诉您它包含多少内容——它可能包含额外的空间,而额外的可用空间量很难判断或预测。
一个巧妙的替代方案是将列表做成对(value, pointer)
,其中每个指针指向下一个元组。这样,你可以逐步增加列表,尽管使用的总内存更高。这是一个链接列表(Python 使用的更像是一个向量或动态数组)。
Eli 的回答非常出色,他解释了[]
和 的[1]
分配是否准确,但将 附加到 会[]
分配额外的块。代码中的注释就是我上面所说的(这称为“过度分配”,其数量与我们拥有的量成比例,因此平均(“摊销”)成本与大小成比例)。
解决方案 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
解决方案 4:
公式根据系统架构而变化,32 位计算机为 (size-36)/4,64 位计算机为 (size-64)/8
36,64 - 基于机器的空列表的大小 4,8 - 基于机器的列表中单个元素的大小
- 2025年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理必备:盘点2024年13款好用的项目管理软件
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)