为什么元组的内存空间less于列表?

一个tuple在Python中占用较less的内存空间:

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

list s需要更多的内存空间:

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

Python内存pipe理的内部会发生什么?

我假设你正在使用CPython和64位(我在CPython 2.7 64位上得到了相同的结果)。 在其他Python实现中可能有区别,或者如果你有一个32位的Python。

无论实现如何, list s是可变大小的,而tuple s是固定大小的。

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

但是还有另外一个list做:他们超额分配。 否则, list.append 总是一个O(n)操作 – 使其分摊O(1) (快得多!!!)。 但是现在必须跟踪分配的大小和填充的大小( tuple只需要存储一个大小,因为分配和填充的大小总是相同的)。 这意味着每个列表必须存储另一个“大小”,在64位系统上是64位整数,也是8个字节。

所以list需要比tuple s多至less16个字节的内存。 为什么我说“至less”? 由于过度分配。 超额分配意味着分配的空间超过需求。 但是,超额分配的数量取决于您如何创build列表和附加/删除历史logging:

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

图片

我决定创build一些图片来陪同上面的解释。 也许这些是有帮助的

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

在这里输入图像说明

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

在这里输入图像说明

有用的链接:

  • Python 2.7的CPython存储库中的tuple结构
  • 在Python 2.7的CPython存储库中list结构体
  • 在Python 2.7的CPython存储库中的int结构

请注意, __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 (实际上将GC开销添加到从__sizeof__返回的值)。

我将深入探讨CPython的代码库,以便我们看到如何计算大小。 在你的具体例子中没有执行超额分配,所以我不会涉及到这一点

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


list s的大小是从以下函数list_sizeof计算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)是一个macros,它ob_type selfob_type (返回PyList_Type ),而_PyObject_SIZE是另一个从该typestp_basicsizemacros。 tp_basicsize计算为sizeof(PyListObject) ,其中PyListObject是实例结构。

PyListObject结构有三个字段:

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

这些有评论(我修剪)解释他们是什么,请按照上面的链接阅读。 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 

即实例struct的大小。


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); } 

就像list s一样,抓取tp_basicsize ,如果对象具有非零的tp_itemsize (意味着它具有可变长度的实例),它将元组中的Py_SIZE数量(通过Py_SIZE获取)与tp_itemsize

tp_basicsize再次使用PyTupleObject结构包含的 sizeof(PyTupleObject)

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

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

 >>> ().__sizeof__() 24 

是吧? 那么,这里有一个古怪的地方,我没有find一个解释, tuple s的tp_basicsize实际上是按如下计算的:

 sizeof(PyTupleObject) - sizeof(PyObject *) 

为什么从tp_basicsize删除额外的8个字节是我一直无法find的东西。 (见MSeifert对可能的解释的评论)


但是,这在你的具体例子中基本上是不同的。 list也围绕着一些分配的元素,这有助于确定何时再次分配。

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

MSeifert答案涵盖广泛; 保持简单你可以想到:

tuple是不可变的。 一旦设置,你不能改变它。 因此,您事先知道需要为该对象分配多less内存。

list是可变的。 您可以添加或删除项目。 它必须知道它的大小(对于内部impl)。 它根据需要resize。

有没有免费的餐点 – 这些function与成本。 因此列表的内存开销。

元组的大小是前缀,这意味着在元组初始化时,解释器为包含的数据分配足够的空间,这是它的结束,它是不可变的(不能被修改),而列表是一个可变的对象,因此意味着dynamic分配内存,所以为了避免在每次添加或修改列表时分配空间(分配足够的空间来容纳已更改的数据并将数据复制到该列表中),它会为将来的追加,修改…分配额外空间总结。