元组在 CPython 中是如何实现的?

Posted

技术标签:

【中文标题】元组在 CPython 中是如何实现的?【英文标题】:How is tuple implemented in CPython? 【发布时间】:2012-12-17 14:47:52 【问题描述】:

我一直在尝试了解 CPython 是如何在幕后实现的。 Python 是高级别的很好,但我不喜欢把它当作一个黑盒子。

考虑到这一点,如何实现元组?我看过the source (tupleobject.c),但它超出了我的想象。

我看到PyTuple_MAXSAVESIZE = 20PyTuple_MAXFREELIST = 2000,什么是保存和“空闲列表”? (长度为 20/21 或 2000/2001 的元组之间会有性能差异吗?是什么强制执行最大元组长度?)

【问题讨论】:

【参考方案1】:

需要注意的是,此答​​案中的所有内容均基于我从查看您链接的实现中收集到的内容。

元组的标准实现似乎只是作为一个数组。但是,有很多优化可以加快速度。

首先,如果您尝试创建一个空元组,CPython 将返回一个代表空元组的规范对象。因此,它可以节省大量仅分配单个对象的分配。

接下来,为了避免分配一堆小对象,CPython 为许多小列表回收内存。有一个固定常数 (PyTuple_MAXSAVESIZE) 使得所有小于此长度的元组都有资格回收它们的空间。每当一个长度小于该常量的对象被释放时,与其关联的内存有可能不会被释放,而是将根据其大小存储在“空闲列表”中(下一段中将详细介绍) .这样,如果您需要分配一个大小为 n 的元组,而其中一个已被分配且不再使用,CPython 可以回收旧数组。

空闲列表本身被实现为一个大小为PyTuple_MAXSAVESIZE的数组,存储指向未使用元组的指针,其中数组的第n个元素指向NULL(如果没有额外的大小为n的元组可用)或一个回收的元组大小为 n。如果有多个不同的大小为 n 的元组可以重用,则它们通过将每个元组的第零个入口点指向下一个可以重用的元组,以一种链表的形式链接在一起。 (由于只分配了一个长度为零的元组,因此永远不会有读取不存在的第零个元素的风险)。通过这种方式,分配器可以存储一定数量的每个大小的元组以供重用。为确保这不会使用太多内存,还有第二个常量PyTuple_MAXFREELIST 控制任何存储桶中任何这些链表的最大长度。然后有一个长度为PyTuple_MAXSAVESIZE 的辅助数组,它存储每个给定长度的元组的链表长度,这样就不会超过这个上限。

总而言之,这是一个非常聪明的实现!

【讨论】:

【参考方案2】:

因为在正常操作过程中 Python 会创建和销毁 很多 个小元组,因此 Python 会为此目的保留一个小元组的内部缓存。这有助于减少大量的内存分配和释放流失。出于同样的原因,从 -5 到 255 的小整数被保留(制成单例)。

PyTuple_MAXSAVESIZE 定义控制符合此优化条件的元组的最大大小,PyTuple_MAXFREELIST 定义控制有多少这些元组保留在内存中。当一个长度 PyTuple_MAXSAVESIZE 的元组被丢弃时,如果仍有空间(tupledealloc)将其添加到空闲列表中,以便在 Python 创建新的小元组时重新使用(PyTuple_New )。

Python 在如何存储这些方面有点聪明;对于每个长度 > 0 的元组,它将重用每个缓存元组的第一个元素,将最多 PyTuple_MAXFREELIST 个元组链接到一个链表中。所以free_list数组中的每个元素都是一个Python元组对象的链表,并且这样一个链表中的所有元组都是相同大小的。唯一的例外是空元组(长度为 0);这些中只需要一个,它是一个单例。

所以,是的,对于长度超过 PyTuple_MAXSAVESIZE 的元组,python 保证必须为新的 C 结构单独分配内存,如果您创建 大量丢弃此类元组,这可能会影响性能.

如果你想了解 Python C 的内部结构,我建议你学习Python C API;它将更容易理解 Python 用于在 C 中定义对象、函数和方法的各种结构。

【讨论】:

为了清楚起见,它是缓存包含内容的元组对象,还是“只是”维护一个适当大小的内存块的空闲列表?我的印象是后者,但你的第一段读起来有点像第一段。 @delnan:元组对象本身的缓存,它们只是对象引用的固定列表。内容本身不会被缓存。元组引用的对象有自己的内存块。 这听起来像是函数式语言实现其不可变列表的方式。 (例如,Haskell 中的 Data.List。)很高兴看到 Python 也有它!

以上是关于元组在 CPython 中是如何实现的?的主要内容,如果未能解决你的问题,请参考以下文章

swift学习之元组

元组比较在 Python 中是如何工作的?

为啥元组在 Elixir 中不可枚举?

为啥元组在内存中占用的空间比列表少?

[Python]-4-元组列表与字典

为啥列表有 __reverse__() 特殊方法但元组在 Python 中没有?