【问题标题】:CPython memory allocationCPython 内存分配
【发布时间】:2013-08-29 23:30:31
【问题描述】:

这是一篇受this comment 启发的帖子,内容是关于如何为 CPython 中的对象分配内存。最初,这是在创建列表并在 for 循环中附加到它的上下文中相对于列表理解。

所以这是我的问题:

  1. CPython 中有多少个不同的分配器?
    • 各自的作用是什么?
  2. malloc 何时被调用? (根据this comment 中的内容,列表理解可能不会导致调用malloc
  3. python 在启动时会为自己分配多少内存?
    1. 是否有规则来管理哪些数据结构首先在此内存上获得“dib”?
  4. 当一个对象被删除时,它所使用的内存会发生什么情况(python 是否仍然保留内存以在将来分配给另一个对象,或者 GC 是否为另一个进程释放内存,例如 Google Chrome,使用)?
  5. 什么时候触发 GC?
  6. lists 是动态数组,这意味着它们需要一块连续的内存。这意味着如果我尝试将一个对象附加到一个列表中,而该列表的底层 C 数据结构数组无法扩展,则该数组将被复制到内存的不同部分,其中有更大的连续块可用。那么当我初始化一个列表时,这个数组分配了多少空间呢?
    • 为新数组分配了多少额外空间,该数组现在保存旧列表和附加对象?

编辑:从 cmets 看来,这里的问题太多了。我这样做只是因为这些问题都非常相关。不过,如果是这种情况,我很乐意将其分成几个帖子(请在 cmets 中告诉我这样做)

【问题讨论】:

  • 为什么投反对票?我看到一个合法的问题,这一次并不简单,近乎愚蠢。
  • 可能是因为这非常广泛。这里至少有 6 个问题。
  • C API 文档的Memory Management 章节中回答了大部分问题。其中一些比您要求的更模糊(“......几个特定于对象的分配器”并没有告诉您有多少“几个”),但是如果不选择特定版本并潜水,您将不会获得更多细节进入代码。
  • 我实际上有一篇写了一半的博客文章解释了其中的一些内容。我在一些额外的段落中添加了一些与我的帖子无关的内容(如 GC),并重写了一些内容并将其发布为社区答案。
  • @abarnert:非常感谢您提供如此详细的答案。读起来真的很有趣

标签: python memory-management cpython


【解决方案1】:

C API 文档的Memory Management 章节中回答了大部分问题。

有些文档比您要求的要模糊。有关更多详细信息,您必须转到源代码。除非您选择特定版本,否则没有人愿意这样做。 (至少 2.7.5、pre-2.7.6、3.3.2、pre-3.3.3 和 pre-3.4 会对不同的人感兴趣。)

obmalloc.c 文件的源代码是您解决许多问题的良好起点,顶部的 cmets 有一个漂亮的小 ASCII 艺术图:

    Object-specific allocators
    _____   ______   ______       ________
   [ int ] [ dict ] [ list ] ... [ string ]       Python core         |
+3 | <----- Object-specific memory -----> | <-- Non-object memory --> |
    _______________________________       |                           |
   [   Python`s object allocator   ]      |                           |
+2 | ####### Object memory ####### | <------ Internal buffers ------> |
    ______________________________________________________________    |
   [          Python`s raw memory allocator (PyMem_ API)          ]   |
+1 | <----- Python memory (under PyMem manager`s control) ------> |   |
    __________________________________________________________________
   [    Underlying general-purpose allocator (ex: C library malloc)   ]
 0 | <------ Virtual memory allocated for the python process -------> |

   =========================================================================
    _______________________________________________________________________
   [                OS-specific Virtual Memory Manager (VMM)               ]
-1 | <--- Kernel dynamic storage allocation & management (page-based) ---> |
    __________________________________   __________________________________
   [                                  ] [                                  ]
-2 | <-- Physical memory: ROM/RAM --> | | <-- Secondary storage (swap) --> |

CPython 中有多少种不同的分配器?

根据文档,“几个”。您可以计算内置和 stdlib 类型中的那些,然后添加少数通用类型,如果您真的想要的话。但我不确定它会告诉你什么。 (这将是非常特定于版本的。IIRC,确切的数字甚至在 3.3 树中发生了变化,因为有一个关于新式字符串是否应该使用三个不同的分配器或一个的实验。)


各自的作用是什么?

+3 级的特定于对象的分配器用于值得优化的特定用例。正如文档所说:

例如,整数对象在堆中的管理方式与字符串、元组或字典不同,因为整数意味着不同的存储要求和速度/空间权衡。

在此之下,有各种通用的支持分配器,级别为 +2(以及 +1.5 和可能 +2.5)——至少是一个对象分配器、一个竞技场分配器和一个小块分配器等——但除了首先是私有的实现细节(意味着即使对 C-API 也是私有的;显然所有这些都是 Python 代码私有的)。

在这之下是原始分配器,其功能是在更高级别的分配器需要时向操作系统请求更多内存。


malloc 什么时候被调用?

原始内存分配器(或其堆管理器)应该是唯一调用 malloc 的东西。 (事实上​​,它甚至可能不一定调用malloc;它可能会使用mmapVirtualAlloc 之类的函数。但关键是它是唯一向操作系统请求内存的东西。)有一些Python 核心中的异常,但它们很少相关。

文档明确指出,高级代码永远不应尝试对从malloc 获得的内存中的 Python 对象进行操作。

但是,除了 Python 对象之外,还有很多标准库和扩展模块使用 malloc 来实现

例如,一个 1000x1000 int32 值的 numpy 数组不会分配 100 万个 Python ints,因此它不必通过 int 分配器。相反,它只是 mallocs 一个包含 100 万个 C ints 的数组,并在您访问它们时根据需要将它们包装在 Python 对象中。


python在启动时会为自己分配多少内存?

这是特定于平台的,从代码中很难弄清楚。然而,当我在我的 64 位 Mac 上启动一个新的 python3.3 解释器时,它从 13.1MB 的虚拟内存开始,几乎立即扩展到 201MB。所以,这应该是一个粗略的大致指南。

是否有规则管理哪些数据结构首先在该内存中获得“dib”?

不是真的,不。恶意或有缺陷的特定对象分配器可以立即获取所有预先分配的内存等等,没有什么可以阻止它。


当一个对象被删除时,它所使用的内存会发生什么(python 是否仍然保留内存以在将来分配给另一个对象,或者 GC 是否为另一个进程释放内存,比如谷歌浏览器,使用)?

它返回到特定于对象的分配器,它可能将其保留在空闲列表中,或者将其释放到原始分配器,保留其自己的空闲列表。原始分配器几乎从不将内存释放回操作系统。

这是因为通常没有充分的理由将内存释放回现代操作系统。如果您有大量未使用的页面,操作系统的虚拟机只会在另一个进程需要时将它们分页。当有充分的理由时,它几乎总是特定于应用程序,最简单的解决方案是使用多个进程来管理你巨大的短期内存需求。


什么时候触发 GC?

这取决于你所说的“GC”是什么意思。

CPython 使用引用计数;每次释放对对象的引用时(通过重新绑定集合中的变量或槽,让变量超出范围等),如果它是最后一个引用,它将立即被清除。这在文档的Reference Counting 部分进行了解释。

但是,引用计数存在一个问题:如果两个对象相互引用,即使所有外部引用都消失了,它们仍然不会被清除。因此,CPython 一直有一个循环收集器,它定期遍历对象,寻找相互引用但没有外部引用的对象的循环。 (这有点复杂,但这是基本思想。)gc 模块的文档中对此进行了充分解释。收集器可以在您明确要求它时运行,当空闲列表变低时,或者当它很长时间没有运行时;这是动态的并且在某种程度上是可配置的,因此很难对“何时”给出具体的答案。


列表是动态数组,这意味着它们需要一块连续的内存。这意味着,如果我尝试将一个对象附加到一个列表中,而该列表的底层 C 数据结构数组无法扩展,则该数组将被复制到内存的不同部分,其中有更大的连续块可用。那么我初始化一个list的时候给这个数组分配了多少空间呢?

这方面的代码主要在listobject.c 中。情况很复杂;有很多特殊情况,例如 timsort 用于创建临时中间列表和非就地排序的代码。但最终,某些代码决定它需要 N 个指针的空间。

这也不是特别有趣。大多数列表要么永远不会扩展,要么扩展得远远超出原始大小,因此在开始时进行额外分配会浪费静态列表的内存,并且对大多数增长的列表没有多大帮助。所以,Python 玩得很保守。我相信它首先查看它的内部 freelist,它不比 N 指针大太多(它也可能合并相邻的释放列表存储;我不知道是否这样做),所以它可能偶尔会过度分配,但通常它没有。确切的代码应该在PyList_New

无论如何,如果列表分配器的空闲列表中没有空间,它就会下降到对象分配器,以此类推;它可能最终会达到 0 级,但通常不会。

为新数组分配了多少额外空间,该数组现在保存旧列表和附加对象?

这是在list_resize 中处理的,这是有趣的部分。

避免list.append 二次方的唯一方法是在几何上过度分配。在最初的几次扩展中,过度分配太小(如 1.2)会浪费太多时间;使用太大的因子(如 1.6)会为非常大的数组浪费太多空间。 Python 通过使用从 2.0 开始但很快收敛到 1.25 左右的序列来处理这个问题。根据3.3来源:

增长模式为:0,4,8,16,25,35,46,58,72,88,...


你没有具体询问sorted,但我知道这是促使你的原因。

请记住,timsort 主要是一种合并排序,对尚未排序的小型子列表进行插入排序。因此,它的大部分操作涉及分配一个大小约为 2N 的新列表并释放两个大小约为 N 的列表。因此,它在复制时几乎可以像在原地复制一样有效地节省空间和分配。最多有 O(log N) 浪费,但这通常不是导致复制排序变慢的因素。

【讨论】:

  • 感谢提及sorted。为了其他可能想知道的人的利益,abarnert 指的是 this post 上的讨论
猜你喜欢
  • 1970-01-01
  • 2020-08-21
  • 2021-03-05
  • 1970-01-01
  • 2011-09-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多