Python Pymalloc 内存分配器:对象大小分类、Arena 与 Pools 的优化策略
大家好,今天我们深入探讨 Python 的 Pymalloc 内存分配器,这是 Python 解释器为了优化小对象内存分配而设计的一个关键组件。理解 Pymalloc 的工作原理,能够帮助我们更好地理解 Python 的性能特性,并可能在一些特定的场景下,优化我们的代码。
1. 为什么要引入 Pymalloc?
在 Python 中,一切皆对象。这意味着即使是最简单的整数、字符串,也都是对象。如果每次创建对象都向操作系统申请内存,销毁对象又释放内存,频繁的内存分配和释放将会带来巨大的开销。
传统的 malloc 和 free 系统调用是通用的内存管理函数,它们设计用于处理各种大小的内存请求,并且维护着全局的堆状态。这意味着:
- 较高的开销: 每次调用
malloc和free都有一定的开销,包括查找合适的内存块、更新堆状态等等。 - 碎片化: 频繁分配和释放不同大小的内存块容易导致内存碎片,降低内存利用率。
- 锁竞争: 在多线程环境中,对全局堆的访问需要加锁,这会引入额外的开销。
为了解决这些问题,Python 引入了 Pymalloc 内存分配器,它专门针对小对象进行优化。
2. Pymalloc 的核心概念
Pymalloc 的核心思想是将内存预先分配成固定大小的块,然后按需分配给小对象。它主要包含以下几个概念:
- Arena: Arena 是 Pymalloc 中最大的内存块,通常大小为 256KB。 Arena 由操作系统通过
malloc分配,并且Arena一旦被分配,就不会被释放,直到 Python 解释器退出。 - Pool: Arena 被划分为多个 Pool。 Pool 的大小通常是 4KB (一个操作系统的内存页大小)。每个 Pool 只能存储特定大小的对象。
- Block: Pool 又被进一步划分为多个 Block。 每个 Block 存储一个对象。同一个 Pool 中的所有 Block 大小相同。
3. 对象大小分类
Pymalloc 只负责分配小于等于 512 字节的小对象。对于大于 512 字节的对象,Python 仍然使用底层的 malloc 和 free 进行分配和释放。
Pymalloc 将小对象的大小划分为多个等级,每个等级对应一个 Pool。 具体的等级划分取决于 Python 的版本。在 Python 3.7 之后,默认的等级划分如下,但可以通过编译选项进行修改:
| 对象大小范围 (字节) | Block Size (字节) |
|---|---|
| 0 < size <= 8 | 8 |
| 8 < size <= 16 | 16 |
| 16 < size <= 24 | 24 |
| 24 < size <= 32 | 32 |
| 32 < size <= 40 | 40 |
| 40 < size <= 48 | 48 |
| 48 < size <= 56 | 56 |
| 56 < size <= 64 | 64 |
| 64 < size <= 72 | 72 |
| 72 < size <= 80 | 80 |
| 80 < size <= 88 | 88 |
| 88 < size <= 96 | 96 |
| 96 < size <= 104 | 104 |
| 104 < size <= 112 | 112 |
| 112 < size <= 120 | 120 |
| 120 < size <= 128 | 128 |
| 128 < size <= 136 | 136 |
| 136 < size <= 144 | 144 |
| 144 < size <= 152 | 152 |
| 152 < size <= 160 | 160 |
| 160 < size <= 168 | 168 |
| 168 < size <= 176 | 176 |
| 176 < size <= 184 | 184 |
| 184 < size <= 192 | 192 |
| 192 < size <= 200 | 200 |
| 200 < size <= 208 | 208 |
| 208 < size <= 216 | 216 |
| 216 < size <= 224 | 224 |
| 224 < size <= 232 | 232 |
| 232 < size <= 240 | 240 |
| 240 < size <= 248 | 248 |
| 248 < size <= 256 | 256 |
| 256 < size <= 264 | 264 |
| 264 < size <= 272 | 272 |
| 272 < size <= 280 | 280 |
| 280 < size <= 288 | 288 |
| 288 < size <= 296 | 296 |
| 296 < size <= 304 | 304 |
| 304 < size <= 312 | 312 |
| 312 < size <= 320 | 320 |
| 320 < size <= 328 | 328 |
| 328 < size <= 336 | 336 |
| 336 < size <= 344 | 344 |
| 344 < size <= 352 | 352 |
| 352 < size <= 360 | 360 |
| 360 < size <= 368 | 368 |
| 368 < size <= 376 | 376 |
| 376 < size <= 384 | 384 |
| 384 < size <= 392 | 392 |
| 392 < size <= 400 | 400 |
| 400 < size <= 408 | 408 |
| 408 < size <= 416 | 416 |
| 416 < size <= 424 | 424 |
| 424 < size <= 432 | 432 |
| 432 < size <= 440 | 440 |
| 440 < size <= 448 | 448 |
| 448 < size <= 456 | 456 |
| 456 < size <= 464 | 464 |
| 464 < size <= 472 | 472 |
| 472 < size <= 480 | 480 |
| 480 < size <= 488 | 488 |
| 488 < size <= 496 | 496 |
| 496 < size <= 504 | 504 |
| 504 < size <= 512 | 512 |
当需要分配一个小于等于 512 字节的对象时,Pymalloc 会找到对应大小等级的 Pool,然后从 Pool 中分配一个 Block。
4. Arena 的管理
Arena 的管理是 Pymalloc 的核心。当 Pymalloc 需要分配内存时,它首先检查是否有可用的 Arena。如果没有,则会调用 malloc 向操作系统申请一个新的 Arena。
Arena 的状态可以分为三种:
- Empty: Arena 中没有任何 Pool。
- Partial: Arena 中有一些 Pool 是空的,还有一些 Pool 已经被使用。
- Full: Arena 中所有的 Pool 都已经被使用。
Pymalloc 维护着一个可用 Arena 链表,它会优先从 Partial Arena 中分配 Pool。 如果找不到合适的 Partial Arena,则会使用 Empty Arena,或者申请新的 Arena。
5. Pool 的管理
每个 Pool 只能存储特定大小的对象。 Pool 的状态也可以分为三种:
- Empty: Pool 中所有的 Block 都是空的。
- Used: Pool 中有一些 Block 已经被使用,还有一些 Block 是空的。
- Full: Pool 中所有的 Block 都已经被使用。
Pymalloc 维护着一个 freeblocks 链表,它指向 Pool 中空闲的 Block。 当需要分配一个对象时,Pymalloc 会从对应大小等级的 Pool 的 freeblocks 链表中取出一个 Block。 当一个对象被释放时,Pymalloc 会将该 Block 添加回 freeblocks 链表。
6. Pymalloc 的优化策略
Pymalloc 通过以下几种策略来优化内存分配:
- 内存池化: 将内存预先分配成固定大小的块,避免了频繁的
malloc和free调用。 - 对象大小分类: 根据对象的大小选择合适的 Pool,避免了内存碎片。
- 缓存机制: 维护着可用 Arena 链表和 freeblocks 链表,加快了内存分配的速度。
- 线程安全: Pymalloc 使用锁来保护其内部数据结构,保证了线程安全。
7. 代码示例
虽然我们无法直接访问 Pymalloc 的内部实现,但我们可以通过一些技巧来观察其行为。
import sys
# 获取对象的大小
size = sys.getsizeof(1) # 整数对象
print(f"Integer object size: {size} bytes")
size = sys.getsizeof("hello") # 字符串对象
print(f"String object size: {size} bytes")
# 观察对象分配的地址
a = 1
b = 1
print(f"Address of a: {id(a)}")
print(f"Address of b: {id(b)}")
# 观察 intern 机制对字符串的影响
a = "hello"
b = "hello"
print(f"Address of a: {id(a)}")
print(f"Address of b: {id(b)}")
a = "hello world"
b = "hello world"
print(f"Address of a: {id(a)}")
print(f"Address of b: {id(b)}")
在这个例子中,我们可以看到:
- 整数对象的大小是固定的,这取决于 Python 的实现。
- 对于较小的字符串,Python 可能会使用 intern 机制,使得相同的字符串对象指向同一个内存地址。
- 对于较大的字符串,Python 会分配不同的内存地址。
这些行为都与 Pymalloc 的内存分配策略有关。
8. Pymalloc 的优缺点
优点:
- 提高内存分配效率: 减少了
malloc和free的调用次数,提高了内存分配的速度。 - 减少内存碎片: 通过对象大小分类,避免了内存碎片。
- 线程安全: 在多线程环境中,能够安全地分配和释放内存。
缺点:
- 内存浪费: 由于 Pool 中的 Block 大小是固定的,可能会造成一定的内存浪费。 例如,如果一个对象只需要 9 个字节,但它会被分配一个 16 字节的 Block,浪费了 7 个字节。
- 只适用于小对象: 对于大于 512 字节的对象,仍然需要使用
malloc和free。 - 增加代码复杂度: Pymalloc 的实现比较复杂,增加了 Python 解释器的代码复杂度。
9. 如何禁用 Pymalloc
在某些情况下,你可能希望禁用 Pymalloc,例如,在调试内存问题时。 你可以通过设置 PYTHONMALLOC 环境变量来禁用 Pymalloc。
PYTHONMALLOC=malloc: 使用系统的malloc和free函数。PYTHONMALLOC=debug: 使用系统的malloc和free函数,并启用内存调试功能。
10. 总结:理解 Pymalloc 的重要性
Pymalloc 是 Python 解释器中一个重要的组成部分,它通过内存池化、对象大小分类等策略,有效地提高了小对象的内存分配效率,减少了内存碎片。 了解 Pymalloc 的工作原理,可以帮助我们更好地理解 Python 的性能特性,并在一些特定的场景下,优化我们的代码。 虽然我们无法直接控制 Pymalloc 的行为,但我们可以通过合理地使用 Python 的数据结构和算法,来减少内存分配的次数,从而提高程序的性能。
更多IT精英技术系列讲座,到智猿学院