CPython的静态对象地址和分片
CPython's static object address and fragmentation
我读了
For CPython, id(x) is the memory address where x is stored.
并且给定对象的 id
永远不会改变,这意味着对象在其生命周期内始终存储在给定的内存地址。这就引出了一个问题:(虚拟)内存碎片怎么办?
假设一个对象A
在地址1(有id
1),占用10个字节,所以它占用地址1-10。对象 B
有 id
11,占用字节 11-12,对象 C
占用地址 13-22。一旦 B 超出范围并获得 GC,我们就会产生碎片。
这个难题是如何解决的?
CPython 为小对象使用它自己的内存分配器,pymalloc-allocator. A quite good description can be found in the code itself。
这个分配器非常擅长避免内存碎片,因为它有效地重用了释放的内存。然而,这只是一种启发式方法,可以想出导致内存碎片的场景。
让我们来看看当我们分配一个大小为 1 字节的对象时会发生什么。
CPython 对于小于 512 字节的对象有自己所谓的 arena。显然,1字节请求将由其分配器管理。
请求的大小分为 64 个不同的 classes:第 0-class 用于 1..8 字节的大小,1th-class 用于大小或 9.. 16 个字节等等 - 这是由于需要对齐 8 个字节。以上每个 classes 都有自己或多或少的 independent/dedicated 内存。我们的要求是第 0-class。
让我们假设这是第一次请求这个大小 - class。将创建一个新的 "pool" 或重新使用一个空池。池是 4KB big,因此有 space 512 个 8 字节 "blocks"。尽管请求只有 1 个字节,但我们将阻塞占用块的另外 7 个字节,因此它们不能用于其他对象。所有空闲块都保存在一个列表中——一开始所有 512 个块都在这个列表中。分配器从这个空闲块列表中删除第一个块和returns它的地址作为指针。
池本身标记为 "used" 并添加到第 0-class 个已用池列表中。
现在,分配另一个大小 <=8 字节的对象发生如下。首先,我们查看 0th-class 的已用池列表,并会找到一个已在使用的池,即有一些已用块和一些空闲块。分配器使用第一个空闲块,将其从空闲块列表中删除,returns 它的地址作为指针。
删除第一个对象很容易 - 我们将占用的块添加为(到目前为止单个)已用池中空闲块列表的头部。
当创建一个8字节的新对象时,使用空闲块列表中的第一个块,这是第一个对象使用的块,现在已删除。
如您所见,内存得到重用,因此内存碎片大大减少。这并不意味着不能有内存碎片:
分配 512 个 1 字节对象后,第一个池变为 "full",第 0-class 个大小的新池将是 created/used。我们还添加了另外 512 个对象,第二个池也变为 "full"。等等。
现在,如果前 511 个元素被删除 - 仍然会有一个字节阻塞整个 4KB,不能用于 other classes。
只有当最后一个块被释放时,池才会变为 "empty",因此可以重新用于其他大小 - classes。
空池不会返回 OS,而是留在竞技场中重复使用。然而,pymalloc manages multiple arenas,如果一个竞技场变成 "unused" 它可能会被释放并且占用的内存(即池)返回到 OS.
我读了
For CPython, id(x) is the memory address where x is stored.
并且给定对象的 id
永远不会改变,这意味着对象在其生命周期内始终存储在给定的内存地址。这就引出了一个问题:(虚拟)内存碎片怎么办?
假设一个对象A
在地址1(有id
1),占用10个字节,所以它占用地址1-10。对象 B
有 id
11,占用字节 11-12,对象 C
占用地址 13-22。一旦 B 超出范围并获得 GC,我们就会产生碎片。
这个难题是如何解决的?
CPython 为小对象使用它自己的内存分配器,pymalloc-allocator. A quite good description can be found in the code itself。
这个分配器非常擅长避免内存碎片,因为它有效地重用了释放的内存。然而,这只是一种启发式方法,可以想出导致内存碎片的场景。
让我们来看看当我们分配一个大小为 1 字节的对象时会发生什么。
CPython 对于小于 512 字节的对象有自己所谓的 arena。显然,1字节请求将由其分配器管理。
请求的大小分为 64 个不同的 classes:第 0-class 用于 1..8 字节的大小,1th-class 用于大小或 9.. 16 个字节等等 - 这是由于需要对齐 8 个字节。以上每个 classes 都有自己或多或少的 independent/dedicated 内存。我们的要求是第 0-class。
让我们假设这是第一次请求这个大小 - class。将创建一个新的 "pool" 或重新使用一个空池。池是 4KB big,因此有 space 512 个 8 字节 "blocks"。尽管请求只有 1 个字节,但我们将阻塞占用块的另外 7 个字节,因此它们不能用于其他对象。所有空闲块都保存在一个列表中——一开始所有 512 个块都在这个列表中。分配器从这个空闲块列表中删除第一个块和returns它的地址作为指针。
池本身标记为 "used" 并添加到第 0-class 个已用池列表中。
现在,分配另一个大小 <=8 字节的对象发生如下。首先,我们查看 0th-class 的已用池列表,并会找到一个已在使用的池,即有一些已用块和一些空闲块。分配器使用第一个空闲块,将其从空闲块列表中删除,returns 它的地址作为指针。
删除第一个对象很容易 - 我们将占用的块添加为(到目前为止单个)已用池中空闲块列表的头部。
当创建一个8字节的新对象时,使用空闲块列表中的第一个块,这是第一个对象使用的块,现在已删除。
如您所见,内存得到重用,因此内存碎片大大减少。这并不意味着不能有内存碎片:
分配 512 个 1 字节对象后,第一个池变为 "full",第 0-class 个大小的新池将是 created/used。我们还添加了另外 512 个对象,第二个池也变为 "full"。等等。
现在,如果前 511 个元素被删除 - 仍然会有一个字节阻塞整个 4KB,不能用于 other classes。
只有当最后一个块被释放时,池才会变为 "empty",因此可以重新用于其他大小 - classes。
空池不会返回 OS,而是留在竞技场中重复使用。然而,pymalloc manages multiple arenas,如果一个竞技场变成 "unused" 它可能会被释放并且占用的内存(即池)返回到 OS.