【问题标题】:Why do tuples take less space in memory than lists?为什么元组在内存中占用的空间比列表少?
【发布时间】:2018-03-21 16:47:24
【问题描述】:

tuple 在 Python 中占用更少的内存空间:

>>> a = (1,2,3)
>>> a.__sizeof__()
48

lists 占用更多内存空间:

>>> b = [1,2,3]
>>> b.__sizeof__()
64

Python 内存管理内部发生了什么?

【问题讨论】:

  • 我不确定这在内部是如何工作的,但列表对象至少有更多的功能,例如元组没有的附加功能。因此,元组作为一种更简单的对象类型更小是有意义的
  • 我认为这也取决于机器对机器......对我来说,当我检查 a = (1,2,3) 需要 72 并且 b = [1,2,3] 需要 88 时。
  • Python 元组是不可变的。可变对象有额外的开销来处理运行时变化。
  • @Metareven 类型拥有的方法数量不会影响实例占用的内存空间。方法列表及其代码由对象原型处理,但实例只存储数据和内部变量。

标签: python python-2.7 list tuples python-internals


【解决方案1】:

元组的大小是前缀的,即在元组初始化时,解释器为包含的数据分配足够的空间,因此它是不可变的(不能修改)。鉴于列表是可变对象,因此意味着内存的动态分配,因此为避免每次追加或修改列表时分配空间(分配足够的空间以包含更改的数据并将数据复制到其中),它为未来的运行时更改,例如追加和修改。

总结得差不多了。

【讨论】:

    【解决方案2】:

    MSeifert 的回答涵盖了广泛;为了简单起见,您可以想到:

    tuple 是不可变的。一旦设置,您将无法更改它。因此,您提前知道需要为该对象分配多少内存。

    list 是可变的。您可以在其中添加或删除项目。它必须知道它当前的大小。它会根据需要调整大小。

    没有免费的饭菜 - 这些功能是有代价的。因此列表的内存开销。

    【讨论】:

      【解决方案3】:

      我假设您使用的是 64 位的 CPython(我在 CPython 2.7 64 位上得到了相同的结果)。其他 Python 实现或您使用 32 位 Python 时可能存在差异。

      不管实现如何,lists 是可变大小的,而tuples 是固定大小的。

      所以tuples 可以将元素直接存储在结构中,另一方面,列表需要一个间接层(它存储指向元素的指针)。这一间接层是一个指针,在 64 位系统上是 64 位,因此是 8 字节。

      lists 还做了另一件事:他们过度分配。否则list.append 将是一个O(n) 操作总是 - 使其摊销O(1)(更快!!!)它过度分配。但现在它必须跟踪 allocated 大小和 filled 大小(tuples 只需要存储一种大小,因为分配和填充大小始终相同) .这意味着每个列表必须存储另一个“大小”,在 64 位系统上是一个 64 位整数,也是 8 个字节。

      所以lists 至少需要比tuples 多16 字节的内存。为什么我说“至少”?因为过度分配。过度分配意味着它分配了比需要更多的空间。但是,过度分配的数量取决于您“如何”创建列表以及追加/删除历史记录:

      >>> l = [1,2,3]
      >>> l.__sizeof__()
      64
      >>> l.append(4)  # triggers re-allocation (with over-allocation), because the original list is full
      >>> l.__sizeof__()
      96
      
      >>> l = []
      >>> l.__sizeof__()
      40
      >>> l.append(1)  # re-allocation with over-allocation
      >>> l.__sizeof__()
      72
      >>> l.append(2)  # no re-alloc
      >>> l.append(3)  # no re-alloc
      >>> l.__sizeof__()
      72
      >>> l.append(4)  # still has room, so no over-allocation needed (yet)
      >>> l.__sizeof__()
      72
      

      图片

      我决定创建一些图像来配合上面的解释。也许这些有用

      在您的示例中,这就是它(示意性地)存储在内存中的方式。我用红色(徒手)循环强调了不同之处:

      这实际上只是一个近似值,因为 int 对象也是 Python 对象,而 CPython 甚至重用小整数,因此内存中对象的更准确表示(尽管不那么可读)可能是:

      有用的链接:

      请注意,__sizeof__ 并没有真正返回“正确”的大小!它只返回存储值的大小。但是,当您使用 sys.getsizeof 时,结果会有所不同:

      >>> import sys
      >>> l = [1,2,3]
      >>> t = (1, 2, 3)
      >>> sys.getsizeof(l)
      88
      >>> sys.getsizeof(t)
      72
      

      有 24 个“额外”字节。这些是真实的,这是__sizeof__ 方法中未考虑的垃圾收集器开销。那是因为您通常不应该直接使用魔术方法 - 使用知道如何处理它们的函数,在这种情况下:sys.getsizeof(实际上是 adds the GC overhead 到从 __sizeof__ 返回的值)。

      【讨论】:

      • Re "所以列表至少需要比元组多 16 个字节的内存。",那不是 8 个吗?元组的一种尺寸和列表的两种尺寸意味着列表的一种额外尺寸。
      • 是的,列表有一个额外的“大小”(8 字节),但也存储了指向“PyObject 数组”的指针(8 字节),而不是直接将它们存储在结构中(元组的作用)。 8+8=16。
      • 另一个关于list内存分配stackoverflow.com/questions/40018398/…有用链接
      • @vishes_shell 这与问题没有真正的关系,因为问题中的代码根本没有过度分配。但是,如果您想在使用 list() 或列表推导时了解更多关于过度分配的数量,它会很有用。
      • @user3349993 元组是不可变的,因此您不能附加到元组或从元组中删除项目。
      【解决方案4】:

      我将深入研究 CPython 代码库,以便了解实际计算大小的方式。 在您的具体示例中没有执行过度分配,所以我不会涉及到这一点

      我将在这里使用 64 位值,就像你一样。


      lists 的大小由以下函数计算得出,list_sizeof

      static PyObject *
      list_sizeof(PyListObject *self)
      {
          Py_ssize_t res;
      
          res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
          return PyInt_FromSsize_t(res);
      }
      

      这里的Py_TYPE(self) 是一个宏,它获取selfob_type(返回PyList_Type),而_PyObject_SIZE 是另一个从该类型中获取tp_basicsize 的宏。 tp_basicsize 计算为sizeof(PyListObject),其中PyListObject 是实例结构。

      PyListObject structure 具有三个字段:

      PyObject_VAR_HEAD     # 24 bytes 
      PyObject **ob_item;   #  8 bytes
      Py_ssize_t allocated; #  8 bytes
      

      这些有 cmets(我修剪过)解释它们是什么,请按照上面的链接阅读它们。 PyObject_VAR_HEAD 扩展为三个 8 字节字段(ob_refcountob_typeob_size),因此 24 字节贡献。

      所以现在res 是:

      sizeof(PyListObject) + self->allocated * sizeof(void*)
      

      或:

      40 + self->allocated * sizeof(void*)
      

      如果列表实例具有已分配的元素。第二部分计算他们的贡献。 self->allocated,顾名思义,保存已分配元素的数量。

      没有任何元素,列表的大小计算为:

      >>> [].__sizeof__()
      40
      

      即实例结构的大小。


      tuple 对象没有定义tuple_sizeof 函数。相反,他们使用object_sizeof 来计算它们的大小:

      static PyObject *
      object_sizeof(PyObject *self, PyObject *args)
      {
          Py_ssize_t res, isize;
      
          res = 0;
          isize = self->ob_type->tp_itemsize;
          if (isize > 0)
              res = Py_SIZE(self) * isize;
          res += self->ob_type->tp_basicsize;
      
          return PyInt_FromSsize_t(res);
      }
      

      对于lists,这会获取tp_basicsize,如果对象有一个非零tp_itemsize(意味着它有可变长度的实例),它将乘以元组中的项目数(它通过Py_SIZE) 和tp_itemsize 获得。

      tp_basicsize 再次使用sizeof(PyTupleObject),其中PyTupleObject struct contains

      PyObject_VAR_HEAD       # 24 bytes 
      PyObject *ob_item[1];   # 8  bytes
      

      所以,没有任何元素(即Py_SIZE 返回0),空元组的大小等于sizeof(PyTupleObject)

      >>> ().__sizeof__()
      24
      

      嗯?好吧,这是一个我没有找到解释的奇怪之处,tuples 的 tp_basicsize 实际上是这样计算的:

      sizeof(PyTupleObject) - sizeof(PyObject *)
      

      为什么从tp_basicsize 中删除了额外的8 字节是我无法找到的。 (有关可能的解释,请参阅 MSeifert 的评论)


      但是,这基本上是您具体示例中的区别lists 还保留了一些已分配的元素,这有助于确定何时再次过度分配。

      现在,当添加其他元素时,列表确实会执行这种过度分配以实现 O(1) 附加。这导致更大的尺寸,因为 MSeifert 在他的回答中很好地涵盖了。

      【讨论】:

      • 我相信ob_item[1] 主要是一个占位符(所以从基本尺寸中减去它是有道理的)。 tuple 是使用PyObject_NewVar 分配的。我还没有弄清楚细节,所以这只是一个有根据的猜测......
      • @MSeifert 抱歉,已修复:-)。我真的不知道,我记得在过去的某个时候发现它,但我从来没有过多关注它,也许我会在将来的某个时候问一个问题:-)
      猜你喜欢
      • 1970-01-01
      • 2013-12-12
      • 2023-04-09
      • 1970-01-01
      • 2022-01-05
      • 2022-01-28
      • 2023-03-07
      • 2020-03-21
      • 2023-01-03
      相关资源
      最近更新 更多