如果我们看看50 k元素下的集合的调整大小行为:
>>> import sys
>>> s = set()
>>> seen = {}
>>> for i in range(50_000):
... size = sys.getsizeof(s)
... if size not in seen:
... seen[size] = len(s)
... print(f"{size=} {len(s)=}")
... s.add(i)
...
size=216 len(s)=0
size=728 len(s)=5
size=2264 len(s)=19
size=8408 len(s)=77
size=32984 len(s)=307
size=131288 len(s)=1229
size=524504 len(s)=4915
size=2097368 len(s)=19661
这种模式与当集合满了3/5时将后备存储大小增加到四倍,加上PySetObject
的一些可能恒定的开销是一致的:
>>> for i in range(9, 22, 2):
... print(2**i + 216)
...
728
2264
8408
32984
131288
524504
2097368
即使对于更大的集合,类似的模式也会继续,但是调整大小的因子切换到加倍而不是四倍。
报告的小集合的大小是离群值。而不是大小为344字节,即16 * 8 + 216(新创建的空集的存储阵列在第一次调整大小为32个插槽之前有8个插槽可用)sys.getsizeof
仅报告216个字节。
我错过了什么?这些小集合是如何存储的,以便它们只使用216字节而不是344字节?
1条答案
按热度按时间mrfwxfqh1#
Python中的
set
对象由以下 C 结构表示:现在请记住,
getsizeof()
调用对象的__sizeof__
方法,并在对象由垃圾收集器管理的情况下添加额外的垃圾收集器开销。好的,
set
实现了__sizeof__
。现在让我们检查一下生产线
_PyObject_SIZE
只是一个扩展为(typeobj)->tp_basicsize
的宏。这段代码实际上是试图访问
tp_basicsize
插槽,以获取类型为set
的示例的字节大小,在set
的情况下,该类型为sizeof(PySetObject)
。我修改了
set_sizeof
C函数,做了以下修改:编译和运行这些变化,
返回值为216/728字节,因为
sys.getsize
增加了16
字节的GC开销。但这里需要注意的是这条线。
因为对于小表(在第一次调整大小之前)
so->table
只是对固定大小(8
)so->smalltable
(没有分配内存)的引用,所以sizeof(PySetObject)
足以获得大小,因为它还包括存储大小(128(16(size of setentry) * 8)
)。现在,当调整大小发生时会发生什么?它构造了一个全新的表(malloc'艾德),并使用该表代替
so->smalltables
。这意味着调整了大小的集合也执行了128字节的死重(固定大小的小表的大小)沿着malloc的so->table
的大小。