11. 内存分配器

CPython 不断分配内存。每个整数对象、列表对象、框架、元组、字典条目数组、字符串缓冲区、代码对象、异常、模块和函数都需要内存。分配器系统的存在是为了使这些分配快速、结构化、可调试且跨平台可移植。

CPython 不只使用一个分配器。它使用多个分配器域和层。小的 Python 对象通常会通过 CPython 的专用小对象分配器,而较大的缓冲区可能会通过平台分配器。

11.1 为什么 CPython 有自己的分配器

Python 程序创建许多短暂的对象。python for i in range(1_000_000): x = (i, i + 1) 该循环分配许多元组对象和整数引用。如果每个小对象分配都直接进入系统malloc,开销会很大。

CPython 的分配器系统通过以下方式改进了这一点:```text serving small object allocations quickly grouping small allocations into arenas and pools reducing calls into the platform allocator supporting debug hooks separating allocator domains making object allocation behavior predictable enough for internals work


## 11.2 分配器域

CPython 将内存分配分为多个域。

重要的领域是:

|域名 |典型用途|
| ------------- | --------------------------------------------------- |
|原始内存|独立于 Python 对象状态的低级内存 |
|内存|通用Python运行时内存|
|对象内存| Python 对象分配 |

 C API 级别,这些显示为函数系列:```c
PyMem_RawMalloc
PyMem_RawCalloc
PyMem_RawRealloc
PyMem_RawFree

PyMem_Malloc
PyMem_Calloc
PyMem_Realloc
PyMem_Free

PyObject_Malloc
PyObject_Calloc
PyObject_Realloc
PyObject_Free
```这种区别很重要,因为每个域可以有不同的挂钩、约束和调试行为。

一个简单的规则:```text
PyMem_Raw*
    use for memory that may be allocated without an initialized Python runtime

PyMem_*
    use for general Python memory

PyObject_*
    use for memory belonging to Python objects
```扩展代码应与同一系列的分配和释放函数相匹配。

正确的:```c
char *p = PyMem_Malloc(128);
if (p == NULL) {
    return PyErr_NoMemory();
}

/* use p */

PyMem_Free(p);
```错误:```c
char *p = PyMem_Malloc(128);
free(p);                 /* wrong allocator family */
```混合分配器系列可能会损坏内存。

## 11.3 对象分配与对象初始化

分配保留内存。初始化为该内存提供了有效的对象状态。

对于 Python 对象来说,这种区别很重要。

对象分配:```text
reserve memory for object layout
set object header
set type pointer
set reference count
possibly track with GC
```对象初始化:```text
fill fields
store references
validate arguments
establish invariants
```对于用户定义的类:```python
obj = MyClass(1, 2)
```大致流程是:```text
call type machinery
allocate memory for instance
initialize object header
call __new__
call __init__
return initialized object
```对于 C 扩展类型,分配通常通过类型对象:```c
self = (MyObject *)type->tp_alloc(type, 0);
```然后初始化填充字段。

## 11.4`tp_alloc`和`tp_free`每个类型对象都可以指定如何分配和释放实例。

重要类型槽:```text
tp_alloc
tp_free
tp_dealloc

tp_alloc为新对象保留内存。tp_free释放对象的内存。tp_dealloc是特定于类型的析构函数。通常会释放字段然后调用tp_free

简化形状:```c static void MyObject_dealloc(MyObject *self) { Py_XDECREF(self->value); Py_TYPE(self)->tp_free((PyObject *)self); }


这种分离允许不同的对象类型使用不同的分配策略,同时保持特定类型的清理明确。

## 11.5 小对象分配器

CPython 的小对象分配器通常称为`pymalloc`

它针对 Python 对象使用的小内存块进行了优化。

从概念上讲:```text
arena
    large region obtained from system allocator

pool
    fixed-size subdivision inside an arena

block
    one small allocation served to CPython
```层次结构:```text
system allocator
    
arenas
    
pools
    
blocks
```小额分配将四舍五入为大小类别。池服务于一种尺寸级别的块。

这避免了向系统分配器询问每个小对象。

## 11.6 阿里纳斯

竞技场是从底层分配器获得的大内存区域。

从概念上讲:```text
arena
    pool
    pool
    pool
    ...
```Arenas  CPython 批量管理许多小对象分配。

 CPython 需要更多内存来存储小对象时,它会请求一个 arena。那个竞技场分为几个池。池用于服务块。

只有当竞技场中的所有池都空闲时,竞技场才能返回到系统。这意味着即使许多对象被销毁,CPython 也可能保留内存。

这种行为可能会让用户感到惊讶:```text
objects were freed
process RSS did not immediately shrink
```这并不总是表明存在泄漏。内存可以由分配器保留以供重用。

## 11.7 池

泳池是竞技场的一个细分部分。

每个池一次服务一个块大小的类别。

例如:```text
pool A
    32-byte blocks

pool B
    64-byte blocks

pool C
    128-byte blocks
```当一个池被分配到一个大小类别时,该池中的所有块都具有相同的大小。这使得分配和释放操作变得简单。

分配器可以维护具有可用块的池列表。分配一个小对象通常意味着从池中获取下一个可用块。

## 11.8 块和尺寸等级

块是针对一次分配请求返回的内存。

小分配大小将四舍五入到大小类别。

概念示例:```text
request 37 bytes
    rounded to 40 or 48 byte class depending on allocator rules

request 72 bytes
    rounded to matching size class

request too large
    bypass pymalloc and use larger allocator path
```类的确切大小取决于 CPython 版本和构建配置。

重要的想法:```text
small requests use fixed-size pools
large requests use another allocator path
```固定大小的池使分配速度更快,并减少小对象工作负载内的碎片。

## 11.9 空闲列表

除了通用分配器之外,某些对象类型还使用空闲列表。

空闲列表缓存最近销毁的特定类型的对象,以便可以快速重用它们。

CPython 历史上的常见示例包括帧、特定大小的元组、浮点、列表和其他内部对象,尽管确切的空闲列表使用情况会因版本而异。

概念流程:```text
destroy object
    if type-specific free list has room:
        put object memory on free list
    else:
        return memory to allocator

create object
    if free list has cached object:
        reuse it
    else:
        allocate new memory
```空闲列表以内存保留换取速度。

它们可以使紧密循环中的对象分配速度更快,但也意味着释放的对象可能不会立即将内存返回给分配器。

## 11.10 实习和对象重用

有些对象被有意地重复使用。

示例包括:```text
None
True
False
small integers
some strings
empty tuple
interned identifiers
```这种重用减少了分配压力,并可以在某些内部路径中实现更快的比较。

例子:```python
a = "name"
b = "name"
```根据字符串的创建方式,CPython 可能会保留它们。内部字符串对于内部使用的标识符、属性名称和字典键很有用。

对象重用是一种优化。 Python 代码不应依赖于对象标识,除了已记录的单例,例如`None`, `True`, `False`, `NotImplemented` `Ellipsis`

正确的:```python
if value is None:
    ...
```避免:```python
if x is 1000:
    ...
```第二个依赖于特定于实现的对象重用行为。

## 11.11 不朽之物及分配

现代 CPython 对选定的运行时拥有的对象使用不朽对象。

不朽的物体被视为永久存在。引用计数操作可以避免对其产生普通的生命周期影响。

这间接影响分配:```text
some fundamental objects are allocated once
their lifetime is the runtime lifetime
normal deallocation never frees them
```Examples may include singleton-like objects and heavily reused internal constants.

对于扩展作者,规则保持不变:```text
use Py_INCREF and Py_DECREF
do not manually inspect or change ob_refcnt
do not assume ordinary deallocation for every object
```无论对象是会死的还是不朽的,正确的代码都会起作用。

## 11.12 内存碎片

当可用内存存在但被分割成无法满足更大的分配请求或无法干净地返回给操作系统的碎片时,就会发生内存碎片。

CPython 可能会在多个级别上遇到碎片:```text
inside pymalloc pools
inside arenas
inside the system allocator
inside type-specific free lists
inside long-lived Python containers
```示例模式:```python
items = []
for i in range(1_000_000):
    items.append(bytearray(100))

del items
```Python 对象可能会被销毁,但内存行为取决于对象大小、分配器路径、竞技场填充度、空闲列表和系统分配器行为。

RSS 可能会保持较高水平,因为 CPython 预计稍后会重用内存。

## 11.13`tracemalloc`

`tracemalloc`跟踪 Python 内存分配。

例子:```python
import tracemalloc

tracemalloc.start()

data = [str(i) for i in range(100_000)]

current, peak = tracemalloc.get_traced_memory()
print(current, peak)

tracemalloc.stop()
```它可以显示内存分配的位置:```python
import tracemalloc

tracemalloc.start()

data = [bytes(1024) for _ in range(1000)]

snapshot = tracemalloc.take_snapshot()
stats = snapshot.statistics("lineno")

for stat in stats[:10]:
    print(stat)

tracemalloc对于 Python 级别的分配调试很有用。它并不显示每个 C 库所做的每个本机分配。

11.14 调试分配器挂钩

CPython 支持调试内存挂钩,有助于发现分配器的误用。

调试构建和调试分配器模式可以检测以下问题:```text writing before allocated memory writing after allocated memory using memory after free freeing memory with wrong allocator family double free uninitialized memory patterns


调试挂钩通常在分配周围添加填充字节,并使用可识别的字节模式填充内存。

这使得内存损坏更容易在源附近被发现。

## 11.15 分配器家族纪律

分配者家族管教严格。

正确的配对:

|分配|免费|
| ----------------- | ---------------- |
|`PyMem_RawMalloc` | `PyMem_RawFree` |
| `PyMem_Malloc`    | `PyMem_Free`    |
| `PyObject_Malloc` | `PyObject_Free` |
| `malloc`          | `free`|

不正确的配对是错误:```c
void *p = PyObject_Malloc(64);
PyMem_Free(p);              /* wrong */
```也错了:```c
void *p = malloc(64);
PyObject_Free(p);           /* wrong */
```创建内存的分配器必须是释放内存的分配器。

## 11.16 在扩展代码中分配缓冲区

对于非对象缓冲区,首选适当的 Python 分配器系列。

例子:```c
typedef struct {
    PyObject_HEAD
    char *data;
    Py_ssize_t size;
} BufferObject;
```分配:```c
self->data = PyMem_Malloc(size);
if (self->data == NULL) {
    PyErr_NoMemory();
    return -1;
}
self->size = size;
```解除分配:```c
static void
Buffer_dealloc(BufferObject *self)
{
    PyMem_Free(self->data);
    Py_TYPE(self)->tp_free((PyObject *)self);
}
```缓冲区使用`PyMem_*`。对象本身使用类型`tp_alloc``tp_free`

将这些生命周期分开。

## 11.17 对象内存和包含的引用

分配对象内存不会自动管理包含的 Python 引用。

例子:```c
typedef struct {
    PyObject_HEAD
    PyObject *value;
} BoxObject;
```分配给内存`value`,但它还没有有效的引用。

初始化必须安全地设置它:```c
self->value = NULL;
```然后分配拥有的引用`Py_INCREF`或者根据 API 合同接收被盗/新的引用。

释放必须释放拥有的引用:```c
Py_XDECREF(self->value);
```内存分配和引用所有权是相关但独立的系统。

## 11.18 分配失败

分配器可能会失败。

C 扩展代码必须检查分配结果。```c
void *p = PyMem_Malloc(size);
if (p == NULL) {
    PyErr_NoMemory();
    return NULL;
}
```对于对象创建函数,`NULL`通常意味着已设置或必须设置例外。

正确模式:```c
PyObject *obj = PyLong_FromLong(42);
if (obj == NULL) {
    return NULL;
}
```永远不要假设分配成功。 Python 代码可以在内存压力、嵌入式环境、受限容器或模糊测试下运行。

## 11.19 重新分配`PyMem_Realloc`改变内存块的大小。

图案:```c
char *new_data = PyMem_Realloc(self->data, new_size);
if (new_data == NULL) {
    PyErr_NoMemory();
    return -1;
}

self->data = new_data;
self->size = new_size;
```在检查成功之前不要覆盖原始指针。

错误:```c
self->data = PyMem_Realloc(self->data, new_size);
if (self->data == NULL) {
    return -1;       /* old pointer lost */
}
```如果重新分配失败,原来的分配仍然有效。丢失该指针会泄漏内存。

## 11.20 超额分配

有些容器会过度分配以避免在每个追加时重新分配。

列表是标准示例。```python
xs = []
for i in range(100):
    xs.append(i)
```该列表不会为每个追加精确地分配一个新槽位。它以更大的步骤增加容量。

从概念上讲:```text
length = number of used entries
allocated = number of available slots
```当长度达到分配的容量时,CPython 会增加项目数组。

这提供了摊销的高效追加行为。

权衡:```text
fewer reallocations
some unused spare capacity
```## 11.21 缩小容器

容器收缩时可能不会立即返回内存。

例子:```python
xs = list(range(1_000_000))
del xs[:900_000]
```逻辑长度减少。仅在特定条件下内部容量可能会缩小。

这可以避免列表反复缩小和增长时代价高昂的重新分配。

对于内存敏感的代码,创建一个新的紧凑容器有时会有所帮助:```python
xs = xs[:]
```或者:```python
xs = list(xs)
```但先衡量一下。复制可能会很昂贵。

## 11.22 内存视图和缓冲区

有些对象通过缓冲区协议公开内存。

示例:```text
bytes
bytearray
array.array
memoryview
mmap objects
NumPy arrays
some extension objects
```缓冲区导出器可能会将原始内存暴露给另一个对象。这造成了生命周期的限制。

例子:```python
b = bytearray(b"hello")
v = memoryview(b)
```当内存视图存在时,调整底层字节数组的大小可能会受到限制。

 C 级别,缓冲区导出器必须确保在消费者持有缓冲区视图时内存保持有效。

这是与分配器相关的,因为当外部视图依赖于内存时,内存无法被释放或移动。

## 11.23 不移动分配器的后果

CPython 的对象指针是稳定的。压缩垃圾收集器通常不会移动对象。

结果:```text
PyObject * pointers remain valid while references are owned
C extensions can store object pointers
id(obj) can be address-like in CPython
memory cannot be compacted by moving live objects
fragmentation can accumulate
```非移动设计是 C API 兼容性的核心。

它还解释了为什么 CPython 使用 arenas、池、空闲列表和仔细的分配器分层而不是压缩堆。

## 11.24 分配器定制

CPython 允许嵌入器和专用环境自定义分配器。

这对于:```text
embedding Python in another application
sandboxing
memory accounting
debugging
custom allocation strategies
instrumentation
constrained runtimes
```分配器定制必须小心进行,通常是在运行时初始化的早期。

替换分配器必须遵守 CPython 对每个分配器域的期望。

错误的分配器挂钩可能会损坏解释器。

## 11.25 内存统计很难

理解 Python 内存使用情况很困难,因为多个层相互作用。

单个 Python 对象可能涉及:```text
object header
object payload
auxiliary arrays
referenced objects
allocator padding
pool overhead
arena overhead
free-list retention
system allocator metadata
native library allocations
```例子:```python
xs = ["abc" for _ in range(1000)]
```内存包括:```text
list object
list item array
1000 references in the array
string objects
string character data
allocator overhead
possibly interned or reused objects

sys.getsizeof(xs)仅报告列表对象的大小及其直接存储,而不报告完整的传递图。

11.26sys.getsizeof

sys.getsizeof返回一个对象所报告的该对象的大小。```python import sys

xs = [1, 2, 3] print(sys.getsizeof(xs))


例子:```python
import sys

xs = [[1], [2], [3]]

print(sys.getsizeof(xs))
```这包括外部列表的存储,而不是内部列表及其内容。

递归大小函数必须仔细遍历引用并避免重复计算共享对象。

## 11.27 常见分配模式

常见的 CPython 分配模式包括:

|图案|示例|分配行为|
| ------------------- | ----------------------- | -------------------------------------------------------- |
|许多小元组 |解析器、AST 工作、循环 |小对象分配器和空闲列表很重要
|大字节对象 | I/O、序列化 |可能会绕过小对象分配器
|不断增长的名单 |大量追加代码 |超额分配事宜|
|大词典 |索引、JSON、全局变量 |哈希表增长很重要|
|框架|函数调用 |帧分配和重用很重要|
|例外 |错误较多的路径 |回溯和帧保留问题|
|字符串|标识符,解析 | Unicode 布局和实习很重要

绩效工作通常从找出哪种模式占主导地位开始。

## 11.28 C扩展分配规则

扩展作者的实用规则:

|情况|规则|
| ----------------------------------- | -------------------------------------- |
|分配Python对象实例|使用型配置机械|
|释放Python对象实例|使用`tp_free`来自释放器 |
|分配辅助运行时内存 |使用`PyMem_*`或有记录的家庭|
|手动分配对象内存|使用`PyObject_*`仅在适当时|
|返回Python对象|返回拥有的参考|
|存储Python对象字段|拥有一个参考 |
|重新分配内存|保留旧指针直到成功|
|处理分配失败|设置或传播`MemoryError`|
|混合分配器|不要这样做 |

分配器错误通常很严重。它们可能看起来像是崩溃,与实际错误相去甚远。

## 11.29 心智模型

使用这个模型:```text
Python object allocation
    type object chooses allocation path
    object memory contains common header
    object-specific fields are initialized
    references are owned explicitly
    deallocator releases references
    tp_free releases memory

Small-object allocation
    arenas contain pools
    pools contain fixed-size blocks
    small requests are served quickly
    memory may be retained for reuse
```CPython 中的内存管理是一个分层系统:```text
reference counting decides when an object dies
cyclic GC finds unreachable cycles
deallocator releases object-owned resources
allocator reuses or frees memory blocks
system allocator manages process heap pages
operating system manages virtual memory
```每一层都回答不同的问题。

## 11.30 总结

CPython 的内存分配器系统支持对 Python 程序的对象密集型工作负载进行快速分配。小物体通常通过`pymalloc`,它将内存组织成竞技场、池和块。特定于类型的空闲列表和对象重用进一步降低了公共对象的分配成本。

对于 C 扩展作者来说,关键规则是分配器系列规则、分配失败的正确处理、安全重新分配以及内存所有权和引用所有权之间的明确分离。

 Python 对象级别释放的内存可能会保留在 CPython 或平台分配器内。对象删除后的高 RSS 并不自动意味着泄漏。 CPython 通常会保留内存以供重用。