【问题标题】:Which takes less memory, a frozenset or a tuple?哪个需要更少的内存,frozenset 还是 tuple?
【发布时间】:2019-07-07 04:44:14
【问题描述】:

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

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

每个对象上的标签都很少,我怀疑查找时间是否会很重要。但是:在这里使用元组和冻结集之间是否存在内存差异?还有其他真正的理由使用其中一个吗?

【问题讨论】:

  • 不要忽视购买更多 RAM 的选项。
  • 您可以通过反转事物来节省一些开销-拥有从对象(身份)映射到该类型的标签(如果存在)的 3x 全局字典(tag1Map、tag2Map、tag3Map)。如果标签是稀疏的,这将很有帮助......(您可以节省为每个对象创建集合所带来的开销)
  • @eddiewould 很好的建议!但我不清楚:每个对象最多只有三个标签,但有大约二十个可能的标签可供选择。我将编辑问题。
  • @user2357112 从长远来看,这可能是一个更好的解决方案……
  • @Draconis 我认为我的建议仍然有效。但是,如果您仅限于一组约 20 个可能的标签,您可以考虑某种位图(标志)方法,即不存储字符串,只需将给定标志的存在/不存在存储为“位”之一在 32 位整数上。然后,您将在其他地方进行(一次)从标志值到实际字符串的映射。

标签: python memory-management tuples frozenset


【解决方案1】:

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

对于足够近的 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 个块的大小类是我们想要的 ;-) 每个块消耗 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 的小对象分配器实际需要使用的字节数。

例如,

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

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

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

【讨论】:

  • 就是这样。当。凉爽的。我唯一的挂断是_debugmallocstats() 中的_ 表明它是半公开的。我们应该担心吗?
  • 关心什么? ;-) 下划线是合适的,因为 CPython 的小对象分配器当然是特定于 CPython 的——它是一个实现细节,而不是语言本身定义的东西。
  • 我猜 Python OOP 已经训练我将_ 开头的方法视为(至少)半私有的。我想我把它与你引用的实现细节混淆了,除非我遗漏了什么
  • 仔细查看文档(像往常一样)澄清了这一点docs.python.org/3/library/sys.html#sys._debugmallocstats
【解决方案2】:

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

所有内置对象都将返回正确的结果,但这对于第三方扩展不一定成立,因为它是特定于实现的。

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

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

【讨论】:

    【解决方案3】:

    `如果用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 上测量的。

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-09-01
        • 2013-11-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-02-27
        • 2012-05-21
        相关资源
        最近更新 更多