哪个占用更少的内存,冻结集或元组?

Which takes less memory, a frozenset or a tuple?

我有一个对象需要 "tagged" 具有 0-3 个字符串(在一组 20 种可能性中);这些值都是唯一的,顺序无关紧要。唯一需要对标签执行的操作是检查特定标签是否存在 (specific_value in self.tags)。

但是,内存中同时存在大量这些对象,以至于它突破了我旧计算机 RAM 的极限。所以节省几个字节可以加起来。

每个对象上的标签都很少,我怀疑查找时间是否会很重要。但是:这里使用 tuple 和 frozenset 在内存上有区别吗?有没有其他真正的理由使用一个而不是另一个?

sys.getsizeof 似乎是您想要的 stdlib 选项...但我对您的整个用例感到不安

import sys
t = ("foo", "bar", "baz")
f = frozenset(("foo","bar","baz"))
print(sys.getsizeof(t))
print(sys.getsizeof(f))

https://docs.python.org/3.7/library/sys.html#sys.getsizeof

All built-in objects will return correct results, but this does not have to hold true for third-party extensions as it is implementation specific.

...所以不要对这个解决方案感到满意

编辑:显然@TimPeters 的回答更正确...

如果您想节省内存,请考虑

  • 通过将标签存在的数据结构提取到外部(单例)数据结构中,以牺牲一些优雅来节省一些内存
  • 使用 "flags"(位图)类型的方法,其中每个标签都映射到 32 位整数中的一位。那么你所需要的只是一个从对象(身份)到 32 位整数(标志)的(单例)dict 映射。如果没有标志,则字典中没有条目。

元组非常紧凑。集合基于哈希表,并依赖于 "empty" 个槽来降低哈希冲突的可能性。

对于足够新的 CPython 版本,sys._debugmallocstats() 显示了许多可能有趣的信息。这里下一个64位Python 3.7.3:

>>> from sys import _debugmallocstats as d
>>> tups = [tuple("abc") for i in range(1000000)]

tuple("abc") 创建一个包含 3 个 1 字符字符串的元组,('a', 'b', 'c')。在这里我将编辑掉几乎所有的输出:

>>> d()
Small block threshold = 512, in 64 size classes.

class   size   num pools   blocks in use  avail blocks
-----   ----   ---------   -------------  ------------
...
    8     72       17941         1004692             4

自从我们创建了一百万个元组以来,使用 1004692 个块的大小 class 是我们想要的一个很好的赌注;-) 每个块占用 72 个字节。

改为切换到 frozensets,输出显示它们每个消耗 224 字节,多出 3 倍多一点:

>>> tups = [frozenset(t) for t in tups]
>>> d()
Small block threshold = 512, in 64 size classes.

class   size   num pools   blocks in use  avail blocks
-----   ----   ---------   -------------  ------------
...
   27    224       55561         1000092             6

在这种特殊情况下,您得到的另一个答案恰好给出了相同的结果:

>>> import sys
>>> sys.getsizeof(tuple("abc"))
72
>>> sys.getsizeof(frozenset(tuple("abc")))
224

虽然这通常是正确的,但并非总是如此,因为对象可能需要 分配 比实际需要更多的字节,以满足硬件对齐要求。 getsizeof() 对此一无所知,但 _debugmallocstats() 显示了 Python 的 small-object 分配器实际需要使用的字节数。

例如,

>>> sys.getsizeof("a")
50

在32位的盒子上,实际需要使用52个字节,来提供4字节对齐。在 64 位的盒子上,目前需要 8 字节对齐,所以需要使用 56 字节。在Python 3.8(尚未发布)下,在64位框上需要16字节对齐,并且需要使用64字节。

但忽略所有这些,元组总是需要比具有相同数量元素的任何形式的集合更少的内存 - 甚至比具有相同数量元素的列表更少。

`如果用 recordclass 库中的类型替换元组,则有可能减少内存:

>>> from recordclass import make_arrayclass
>>> Triple = make_arrayclass("Triple", 3)
>>> from sys import getsizeof as sizeof
>>> sizeof(Triple("ab","cd","ef"))
40
>>> sizeof(("ab","cd","ef"))
64

差值等于sizeof(PyGC_Head) + sizeof(Py_ssize_t).

P.S.: 数字是在 64 位 Python 3.8.

上测量的