【问题标题】:Size of list in memory内存中列表的大小
【发布时间】:2011-11-07 00:27:08
【问题描述】:

我刚刚试验了内存中 python 数据结构的大小。我写了以下sn-p:

import sys
lst1=[]
lst1.append(1)
lst2=[1]
print(sys.getsizeof(lst1), sys.getsizeof(lst2))

我在以下配置上测试了代码:

  • Windows 7 64bit,Python3.1:输出为:52 40 所以 lst1 有 52 字节,lst2 有 40 字节。
  • Ubuntu 11.4 32bit with Python3.2:输出为48 32
  • Ubuntu 11.4 32bit Python2.7:48 36

谁能向我解释为什么这两种尺寸不同,尽管它们都是包含 1 的列表?

在 getsizeof 函数的 python 文档中,我发现了以下内容:...adds an additional garbage collector overhead if the object is managed by the garbage collector. 我的小示例中可能是这种情况吗?

【问题讨论】:

  • 列表不是增量分配的,而是在“块”中分配的(随着列表变大,块也会变大)。这是必需的,以便附加数据的摊销成本较低。所以我猜分配器在这两种情况下的工作方式不同。但实际上,您为什么如此关心列表的分配方式?如果您需要知道某物的大小,请使用 sys.getsizeof()。
  • @andrew cooke:请给出答案,这几乎是全部交易。
  • @andrew-cooke 我只是对低级实现感到好奇,不会在实际问题中使用它。
  • @halex:你可以阅读实现,Python 是开源的。
  • @Jochen:我很好奇所以就这么做了。请参阅下面的答案

标签: python python-3.x


【解决方案1】:

抱歉,之前的评论有点草率。

发生的事情是您正在查看列表是如何分配的(我想您可能只是想看看事情有多大 - 在这种情况下,请使用 sys.getsizeof()

当某些东西被添加到列表中时,可能会发生以下两种情况之一:

  1. 多余的物品可以放入备用空间

  2. 需要额外的空间,所以创建了一个新列表,复制了内容,并添加了额外的内容。

因为(2)很昂贵(复制东西,甚至是指针,所花费的时间与要复制的东西的数量成正比,所以随着列表变大而增长)我们希望不经常这样做。因此,我们不仅添加了更多空间,还添加了整个块。通常,添加量的大小与已在使用的量相似 - 这样算出分配内存的平均成本,分布在许多用途中,仅与列表大小成正比。

所以您所看到的与此行为有关。我不知道确切的细节,但如果[][1](或两者)是特殊情况,我不会感到惊讶,其中只分配了足够的内存(在这些常见情况下节省内存),然后appending 执行上述“抓取一个新块”,增加了更多。

但我不知道确切的细节 - 这就是动态数组的一般工作方式。 python 中列表的确切实现将被微调,使其最适合典型的 python 程序。所以我真正想说的是,你不能相信列表的大小可以准确地告诉你它包含多少——它可能包含额外的空间,而额外的可用空间量很难判断或预测。

ps 一个巧妙的替代方法是将列表创建为(value, pointer) 对,其中每个指针指向下一个元组。通过这种方式,您可以逐步增加列表,尽管使用的总内存更高。那是一个链表(python使用的更像是一个向量或者一个动态数组)。

[更新] 请参阅 Eli 的出色回答。他/她解释说[][1] 都被精确分配,但附加到[] 分配了一个额外的块。代码中的注释就是我上面所说的(这称为“过度分配”,数量与我们拥有的数量成正比,因此平均(“摊销”)成本与规模成正比。

【讨论】:

    【解决方案2】:

    这里有一个更完整的交互式会话,可以帮助我解释发生了什么(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_resizesize+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 在一个空列表上被调用时,它用size=1 调用list_resize。根据list_resize的过度分配算法,1之后的下一个最大可用大小是4,因此将分配4个指针的位置。 4 * 4 = 16 字节,36 + 16 = 52。

    确实,一切都有意义:-)

    【讨论】:

    • 这是我遇到的所有编程语言/库中 List.append() 的标准分配策略。我想我会在不阅读您的答案的情况下猜到这是原因(但现在我已经阅读了它,所以我真的不知道)。无论如何,很好的详细答案。
    • @ripper234:是的,分配策略很常见,但我想知道增长模式本身。摊销线性运行时的教科书示例通常提到 2 的幂。这似乎是一个不寻常的模式,仍然满足线性行为的公式 - 我觉得这很有趣
    • 有趣的是,关于“增长模式是:”的评论实际上并没有描述代码中的策略。它从 3 或 6 的基本超额分配开始,具体取决于新大小在 9 的哪一侧,然后每 8 次将 over 分配增加 1。(我怀疑评论早于当前版本过度分配策略)。
    • @teepark:你能详细说明一下吗?我运行了一些粗略的数字,恕我直言,代码根据评论工作。请记住,一旦过度分配给 8,下一个“newsize”请求将是 9。
    • 是的,你是对的。我没有得到的是,它本质上是在跟踪从循环中的追加发生的 realloc(3)。
    【解决方案3】:

    以下是列表增长模式的快速演示。更改 range() 中的第三个参数将更改输出,因此它看起来不像 listobject.c 中的 cmets,但仅附加一个元素时的结果似乎非常准确。

    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】:

      根据系统架构更改公式 (size-36)/4 用于 32 位机器和 (size-64)/8 用于 64 位机器

      36,64 - 基于机器的空列表大小 4,8 - 基于机器的列表中单个元素的大小

      【讨论】:

        【解决方案5】:

        不应将内存中 Python list 的大小与 array 混淆,我认为这就是这里的主要答案中发生的事情。

        是的,根据某些增长模式,可变长度的连续块的大小会增加,并留有余地。从 Python3.8 开始,规则是:

        • 56 字节的空列表
        • 在初始化时分配足够的内存来容纳元素
        • 每次添加物品,如果没有空间,则使用增长算法扩展空间

        所以在 Python 上运行 sys.getsizeof list 提供了这个:

        >>> l0=[]; l1=[1]; l2=[1,2]
        >>> sys.getsizeof(l0), sys.getsizeof(l1), sys.getsizeof(l2)
        (56,64,72)
        

        将一个元素添加到更小的lists 会导致list 比使用完整长度初始化的list 更长:

        >>> l0.append(1); l1.append(1)
        >>> sys.getsizeof(l0), sys.getsizeof(l1),sys.getsizeof(l2)
        (88, 96, 72)
        

        但这仅显示为引用实际数据的指针分配的内存,而实际数据位于别处。在考虑它的大小时,我们还应该计算我们放入 list 的数据。否则,人们可能会得出结论,它们与numpy.array 一样高效。 内存中列表的大小list中的sys.getsizeoflist中每个变量的大小。

        >>> ll=list(range(1000))
        >>> sys.getsizeof(ll)
        8056
        >>> sys.getsizeof(numpy.array(ll)
        8104
        >>> sys.getsizeof(ll)+sum([sys.getsizeof(element) for element in ll])
        36052
        

        因此,Python list 大约是 numpy.array 的 4 倍大,而且速度也比 numpy.array 慢,因为在读/写操作期间这些值不是本地的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-07-27
          • 1970-01-01
          • 2020-01-31
          • 1970-01-01
          • 1970-01-01
          • 2011-11-01
          • 1970-01-01
          相关资源
          最近更新 更多