Python的Pymalloc内存分配器:对象大小分类、Arena与Pools的优化策略

Python Pymalloc 内存分配器:对象大小分类、Arena 与 Pools 的优化策略

大家好,今天我们深入探讨 Python 的 Pymalloc 内存分配器,这是 Python 解释器为了优化小对象内存分配而设计的一个关键组件。理解 Pymalloc 的工作原理,能够帮助我们更好地理解 Python 的性能特性,并可能在一些特定的场景下,优化我们的代码。

1. 为什么要引入 Pymalloc?

在 Python 中,一切皆对象。这意味着即使是最简单的整数、字符串,也都是对象。如果每次创建对象都向操作系统申请内存,销毁对象又释放内存,频繁的内存分配和释放将会带来巨大的开销。

传统的 mallocfree 系统调用是通用的内存管理函数,它们设计用于处理各种大小的内存请求,并且维护着全局的堆状态。这意味着:

  • 较高的开销: 每次调用 mallocfree 都有一定的开销,包括查找合适的内存块、更新堆状态等等。
  • 碎片化: 频繁分配和释放不同大小的内存块容易导致内存碎片,降低内存利用率。
  • 锁竞争: 在多线程环境中,对全局堆的访问需要加锁,这会引入额外的开销。

为了解决这些问题,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 仍然使用底层的 mallocfree 进行分配和释放。

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 通过以下几种策略来优化内存分配:

  • 内存池化: 将内存预先分配成固定大小的块,避免了频繁的 mallocfree 调用。
  • 对象大小分类: 根据对象的大小选择合适的 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 的优缺点

优点:

  • 提高内存分配效率: 减少了 mallocfree 的调用次数,提高了内存分配的速度。
  • 减少内存碎片: 通过对象大小分类,避免了内存碎片。
  • 线程安全: 在多线程环境中,能够安全地分配和释放内存。

缺点:

  • 内存浪费: 由于 Pool 中的 Block 大小是固定的,可能会造成一定的内存浪费。 例如,如果一个对象只需要 9 个字节,但它会被分配一个 16 字节的 Block,浪费了 7 个字节。
  • 只适用于小对象: 对于大于 512 字节的对象,仍然需要使用 mallocfree
  • 增加代码复杂度: Pymalloc 的实现比较复杂,增加了 Python 解释器的代码复杂度。

9. 如何禁用 Pymalloc

在某些情况下,你可能希望禁用 Pymalloc,例如,在调试内存问题时。 你可以通过设置 PYTHONMALLOC 环境变量来禁用 Pymalloc。

  • PYTHONMALLOC=malloc: 使用系统的 mallocfree 函数。
  • PYTHONMALLOC=debug: 使用系统的 mallocfree 函数,并启用内存调试功能。

10. 总结:理解 Pymalloc 的重要性

Pymalloc 是 Python 解释器中一个重要的组成部分,它通过内存池化、对象大小分类等策略,有效地提高了小对象的内存分配效率,减少了内存碎片。 了解 Pymalloc 的工作原理,可以帮助我们更好地理解 Python 的性能特性,并在一些特定的场景下,优化我们的代码。 虽然我们无法直接控制 Pymalloc 的行为,但我们可以通过合理地使用 Python 的数据结构和算法,来减少内存分配的次数,从而提高程序的性能。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注