为什么元组占用的内存比列表少space?
Why do tuples take less space in memory than lists?
A tuple
在 Python 中占用更少的内存 space:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
而 list
s 占用更多内存 space:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Python 内存管理内部发生了什么?
我假设您使用的是 CPython 和 64 位(我在 CPython 2.7 64 位上得到了相同的结果)。其他 Python 实现可能存在差异,或者如果您有 32 位 Python.
无论实现方式如何,list
都是可变大小的,而 tuple
是固定大小的。
所以 tuple
s 可以直接在结构中存储元素,另一方面,列表需要一个间接层(它存储指向元素的指针)。这层间接是一个指针,在 64 位系统上是 64 位,因此是 8 个字节。
但是 list
还做了另一件事:过度分配。否则 list.append
将是一个 O(n)
操作 总是 - 使其摊销 O(1)
(快得多!!!)它会过度分配。但是现在它必须跟踪 allocated 大小和 filled 大小(tuple
s 只需要存储一个大小,因为分配和填充的大小始终相同)。这意味着每个列表必须存储另一个 "size",在 64 位系统上它是一个 64 位整数,同样是 8 个字节。
所以 list
s 需要至少比 tuple
s 多 16 字节的内存。为什么我说 "at least"?因为过度分配。过度分配意味着它分配的 space 比需要的多。但是,过度分配的数量取决于 "how" 您创建的列表和 append/deletion 历史记录:
>>> 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 甚至重用了小整数,因此可能更准确地表示(尽管不那么可读)内存中的对象将是:
有用的链接:
tuple
struct in CPython repository for Python 2.7
list
struct in CPython repository for Python 2.7
int
struct in CPython repository for Python 2.7
请注意 __sizeof__
并不是 return "correct" 大小!它只有 returns 存储值的大小。但是,当您使用 sys.getsizeof
时,结果会有所不同:
>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72
有 24 "extra" 个字节。这些是 真实的 ,这是 __sizeof__
方法中未考虑的垃圾收集器开销。那是因为你通常不应该直接使用魔术方法 - 使用知道如何处理它们的函数,在这种情况下:sys.getsizeof
(which actually adds the GC overhead 到值 returned from __sizeof__
)。
MSeifert 的回答涵盖广泛;为了简单起见,您可以想到:
tuple
是不可变的。一旦设置,您将无法更改。所以你提前知道你需要为那个对象分配多少内存。
list
是可变的。您可以向其中添加或从中删除项目。它必须知道它当前的大小。它会根据需要调整大小。
天下没有免费的饭菜 - 这些功能都是有代价的。因此,列表的内存开销。
我将更深入地研究 CPython 代码库,以便我们了解大小的实际计算方式。 在您的具体示例中,没有执行过度分配,所以我不会涉及。
我将在此处使用 64 位值,就像您一样。
list
s 的大小是根据以下函数计算的,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)
is a macro that grabs the ob_type
of self
(returning PyList_Type
) while _PyObject_SIZE
is another macro that grabs 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
这些有评论(我删减了)解释它们是什么,按照上面的link阅读它们。 PyObject_VAR_HEAD
扩展为三个 8 字节字段(ob_refcount
、ob_type
和 ob_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);
}
这个,对于 list
s,获取 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
returns 0
)空元组的大小等于 sizeof(PyTupleObject)
:
>>> ().__sizeof__()
24
嗯?好吧,这里有一个我还没有找到解释的奇怪之处,tuple
s 的 tp_basicsize
实际上是这样计算的:
sizeof(PyTupleObject) - sizeof(PyObject *)
为什么从 tp_basicsize
中删除了一个额外的 8
字节是我无法找到的。 (有关可能的解释,请参阅 MSeifert 的评论)
但是,这基本上就是您的具体示例中的区别。 list
s 还保留了一些已分配的元素,这有助于确定何时再次过度分配。
现在,当添加额外的元素时,列表确实会执行这种过度分配以实现 O(1) 追加。这导致更大的尺寸,因为 MSeifert 在他的回答中很好地涵盖了。
元组的大小是有前缀的,即在元组初始化时,解释器为包含的数据分配足够的 space,因此它是不可变的(无法修改)。鉴于列表是一个可变对象,因此意味着动态分配内存,因此要避免每次附加或修改列表时分配 space(分配足够的 space 以包含更改的数据并将数据复制到它),它会分配额外的 space 用于将来的运行时更改,例如追加和修改。
总结得差不多了。
A tuple
在 Python 中占用更少的内存 space:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
而 list
s 占用更多内存 space:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
Python 内存管理内部发生了什么?
我假设您使用的是 CPython 和 64 位(我在 CPython 2.7 64 位上得到了相同的结果)。其他 Python 实现可能存在差异,或者如果您有 32 位 Python.
无论实现方式如何,list
都是可变大小的,而 tuple
是固定大小的。
所以 tuple
s 可以直接在结构中存储元素,另一方面,列表需要一个间接层(它存储指向元素的指针)。这层间接是一个指针,在 64 位系统上是 64 位,因此是 8 个字节。
但是 list
还做了另一件事:过度分配。否则 list.append
将是一个 O(n)
操作 总是 - 使其摊销 O(1)
(快得多!!!)它会过度分配。但是现在它必须跟踪 allocated 大小和 filled 大小(tuple
s 只需要存储一个大小,因为分配和填充的大小始终相同)。这意味着每个列表必须存储另一个 "size",在 64 位系统上它是一个 64 位整数,同样是 8 个字节。
所以 list
s 需要至少比 tuple
s 多 16 字节的内存。为什么我说 "at least"?因为过度分配。过度分配意味着它分配的 space 比需要的多。但是,过度分配的数量取决于 "how" 您创建的列表和 append/deletion 历史记录:
>>> 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 甚至重用了小整数,因此可能更准确地表示(尽管不那么可读)内存中的对象将是:
有用的链接:
tuple
struct in CPython repository for Python 2.7list
struct in CPython repository for Python 2.7int
struct in CPython repository for Python 2.7
请注意 __sizeof__
并不是 return "correct" 大小!它只有 returns 存储值的大小。但是,当您使用 sys.getsizeof
时,结果会有所不同:
>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72
有 24 "extra" 个字节。这些是 真实的 ,这是 __sizeof__
方法中未考虑的垃圾收集器开销。那是因为你通常不应该直接使用魔术方法 - 使用知道如何处理它们的函数,在这种情况下:sys.getsizeof
(which actually adds the GC overhead 到值 returned from __sizeof__
)。
MSeifert 的回答涵盖广泛;为了简单起见,您可以想到:
tuple
是不可变的。一旦设置,您将无法更改。所以你提前知道你需要为那个对象分配多少内存。
list
是可变的。您可以向其中添加或从中删除项目。它必须知道它当前的大小。它会根据需要调整大小。
天下没有免费的饭菜 - 这些功能都是有代价的。因此,列表的内存开销。
我将更深入地研究 CPython 代码库,以便我们了解大小的实际计算方式。 在您的具体示例中,没有执行过度分配,所以我不会涉及。
我将在此处使用 64 位值,就像您一样。
list
s 的大小是根据以下函数计算的,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)
is a macro that grabs the ob_type
of self
(returning PyList_Type
) while _PyObject_SIZE
is another macro that grabs 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
这些有评论(我删减了)解释它们是什么,按照上面的link阅读它们。 PyObject_VAR_HEAD
扩展为三个 8 字节字段(ob_refcount
、ob_type
和 ob_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);
}
这个,对于 list
s,获取 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
returns 0
)空元组的大小等于 sizeof(PyTupleObject)
:
>>> ().__sizeof__()
24
嗯?好吧,这里有一个我还没有找到解释的奇怪之处,tuple
s 的 tp_basicsize
实际上是这样计算的:
sizeof(PyTupleObject) - sizeof(PyObject *)
为什么从 tp_basicsize
中删除了一个额外的 8
字节是我无法找到的。 (有关可能的解释,请参阅 MSeifert 的评论)
但是,这基本上就是您的具体示例中的区别。 list
s 还保留了一些已分配的元素,这有助于确定何时再次过度分配。
现在,当添加额外的元素时,列表确实会执行这种过度分配以实现 O(1) 追加。这导致更大的尺寸,因为 MSeifert 在他的回答中很好地涵盖了。
元组的大小是有前缀的,即在元组初始化时,解释器为包含的数据分配足够的 space,因此它是不可变的(无法修改)。鉴于列表是一个可变对象,因此意味着动态分配内存,因此要避免每次附加或修改列表时分配 space(分配足够的 space 以包含更改的数据并将数据复制到它),它会分配额外的 space 用于将来的运行时更改,例如追加和修改。
总结得差不多了。