08月15, 2020

20. Python 内存管理漫游-2

Python 对内存进行了多层抽象:

image.png

  • 0 层就是最基本的 C 库,我们熟悉的 malloc、free 等函数就在这一层;
  • 1 层是对 C 库内存管理接口的封装,基本上只是对 malloc、free 的代理函数,主要是处理 size 为 0 的时候,不同系统的差异性,Python 的策略也非常简单,当 size 为 0 的时候,至少申请 1 字节内存,这样就解决了不同系统之间的差异性;
  • 2 层实现了 Python 高级内存管理系统,这也是我们研究的重点;
  • 3 层就到了对象层,这一层主要是一些针对对象的缓存优化,比如前面提到的小整数数组、拉丁字母表等技术。

我们采用从下向上的研究思路,最底层的 malloc 和 free,只要有过 C 开发经验就会很熟悉。

第 0 ~ 1 层代码非常简单,代码如下:

static void *
_PyMem_RawMalloc(void *ctx, size_t size)
{
    if (size == 0)
        size = 1;
    return malloc(size);
}

static void *
_PyMem_RawCalloc(void *ctx, size_t nelem, size_t elsize)
{
    if (nelem == 0 || elsize == 0) {
        nelem = 1;
        elsize = 1;
    }
    return calloc(nelem, elsize);
}

static void *
_PyMem_RawRealloc(void *ctx, void *ptr, size_t size)
{
    if (size == 0)
        size = 1;
    return realloc(ptr, size);
}

static void
_PyMem_RawFree(void *ctx, void *ptr)
{
    free(ptr);
}

可以证实它们都只是对 C 库最简单的封装,在 Python 2.5 版本中,甚至就是用宏实现的。那么让我们直接向上一层研究。

内存管理的主要目的:

  • 首先,避免内存泄漏;
  • 提高内存分配效率。

避免内存泄漏的办法很多,其中一种比较朴素的办法是将所有已申请的内存加入到链表中,释放时可以确保全部释放。可以参考很久以前实现的一个简单版本(https://github.com/alvendarthy/tree_style_mem_pool)。

这种做法确实可以解决内存泄漏的问题,但内存分配依然使用 malloc 和 free,程序长时间运行后,内存碎片化比较严重,没有解决内存分配效率的问题。

提高内存跟配效率常用的做法是,一次性申请一块大的内存由应用自行管理,应用需要内存时不使用 malloc,而从这块内存中划出所需空间,释放的时候也不调用 free,而是将不再需要的内存归还给内存池。常见的内存管理技术大体思路都与这个差不多,差异主要表现在对内存池的管理环节。

在 Python 中无论通过 PyObject_GC_New 还是直接用较低级接口 PyObject_MALLOC,都不会直接穿透到第 1 层,通常由下面这样的接口处理内存申请:

static void *
_PyObject_Malloc(void *ctx, size_t nbytes)
{
    void* ptr = pymalloc_alloc(ctx, nbytes);
    if (ptr != NULL) {
        _Py_AllocatedBlocks++;
        return ptr;
    }

    ptr = PyMem_RawMalloc(nbytes);
    if (ptr != NULL) {
        _Py_AllocatedBlocks++;
    }
    return ptr;
}

其中 pymalloc_alloc 是从 Python 的内存池中划分空间,PyMem_RawMalloc 实际上就是 C 库中的 malloc 分配空间。Python 决定内存分配来源的条件通过 pymalloc_alloc 的返回值表示,当其返回 Null 时表示出错或者空间大小不符合内存池分配条件,就会转而尝试直接使用 C 库分配。在 pymalloc_alloc 中由这样的判断:

if (nbytes == 0) {
        return NULL;
    }
    if (nbytes > SMALL_REQUEST_THRESHOLD) {
        return NULL;
    }

显然,如果分配空间大小为 0,或者大于 SMALL_REQUEST_THRESHOLD 就转由 malloc 申请内存。在 Python 3.8 中 SMALL_REQUEST_THRESHOLD 是一个宏,其数值是 512,Python 2.5 中是 256,看来新版本扩大了内存池的边界,应该是计算机内存增长的结果。

这样情况就很清楚了,只有小于 512 字节的对象才会在内存池中分配,0 或者 大于 512 字节的对象会直接使用 C lib 接口,直接从系统获得内存.

image.png

明天我们继续深入,研究这些小对象在内存中的创建和管理。

本文链接:http://www.thinkinpython.com/post/deep_python_vm_20.html

-- EOF --