python内存管理

起因

春节期间,翻了翻《垃圾回收的算法和实现》,真是一本好书。时间比较短还没有完全看完。但是让我吃惊的时候,这是一本将垃圾回收的书,但是在它的实现篇里居然对python的内存管理有比较深入的讲解。今天有空,又细细结合python 3.6的代码看了一遍。在这里写下我的这个读书笔记。

intro

python内存管理简图

上面是我结合书中的讲解,把它提到的一些概念用这个图都表达了出来。当然也可以说,我把书中的几张图合成了一张。

其实如果你去看python的源代码,在Objects/obmalloc.c这个文件中,对python是怎么维护内存的有详细的讲解。

概念

arena

这个区域是从堆内存里面直接malloc出来的,每个是256KB。

pool

针对malloc出来的arena,我们会对它进行分割,pool的大小是4K字节。至于为什么是4K字节,书中说这个是因为现在的操作系统大都是以4KB为大小做了内存页的管理单位,把我们的pool的大小也就设为了4KB。

#define SMALL_REQUEST_THRESHOLD 512

static void *
_PyObject_Alloc(int use_calloc, void *ctx, size_t nelem, size_t elsize)
{
...
    if ((nbytes - 1) < SMALL_REQUEST_THRESHOLD) {
    // do all the small objects allocation just as the book describes
    //...
    }
redirect:
    /* Redirect the original request to the underlying (libc) allocator.
     * We jump here on bigger requests, on error in the code above (as a
     * last chance to serve the request) or when the max memory limit
     * has been reached.
     */
    {
        void *result;
        if (use_calloc)
            result = PyMem_RawCalloc(nelem, elsize);
        else
            result = PyMem_RawMalloc(nbytes);
        if (!result)
            _Py_AllocatedBlocks--;
        return result;
    }
}

从上面的代码可以看到,只有对象大小在512字节以内的才会走到我们现在要讨论的这个分配系统,而大于512字节的,则直接走malloc了。

这里的设置跟书中提到的256字节的限制是不同的,可能在不同版本中,这个值得到了修正。

对于usedpool,它里面所维护的都是正在使用的pool,用代码中的话来说

  1. 在它里面的pool至少有一个block是被分配的
  2. 在它里面的pool至少有一个block是没有被分配的

那么那些被分配满block的pool去哪里了呢?它没有在这个usedpool中,它们在堆中处于游离状态。
而那些空的pool,则将它们返回给arena的freepools,由arena继续管理。

当arena里面全都是空pool的时候,这个arena就可以被释放掉了,arena的释放是通过free来完成的。

需要特殊说明的是,在上图中,pool左边我特意画了一块空白的空间出来,这个在书中是没有的,因为根据我的理解和读代码所得,每个pool都带有它自己的管理结构pool_header,也就是这个pool的metadata,它的定义我也在图中写出来了。它里面维护着这个pool中的一些信息,以及一些变量用来在这个pool中进行block分配。

block

有了pool之后,我们就可以在pool中划分出我们的block来了,但是我们在使用python过程中,对象的大小千奇百怪,为了内能适应不同的对象大小要求,python内部,采用了类似于malloc管理内存的方法,针对于每一个大小的对象,我们都会有一个pool与它对应。这要就有了上图中右边的这个结构。

这个结构是为了在分配过程中快速找到相对应的pool的一个结构,它可以在O(1)的时间分配所需要的内存。

usedpools

注意右边这个usedpool的全局变量,从概念上来看,它应该保存的是pool_header的前后关系,但是在在代码的实现中,里面实际上只是保存了pool_header里面的nextpoolprevpool的信息。为什么要这么做,在源代码里作者也给出了答案,这个结构需要经常的变动,如果把pool_header的信息全都放进去的话,会有一些空间上的浪费,使得cpu不能一次把整个结构load到cpu cache中去,或者说是cache line中去。为了防止这种情况的发生,加快对这个结构的访问,才做了这个优化。

pool中的freeblock

下面说说在看代码的过程中,我所遇到的一个问题,通过这个问题更深的了解了在pool中,block的分配是如何进行的。

typedef uint8_t block;

            if ((pool->freeblock = *(block **)bp) != NULL) {
            ...
            }

在看到代码的时候,经常看到*(block **)bp,从代码的上下文来看,这个就是讲pool->freeblock指向下一个freeblock,但是简单的这个指针操作真的就能完成了嘛?

bp本来就是一个block指针,现在把它强转成block指针的指针,也就是说强转成一个指向block指针数组的的指针(简单这么理解),然后再解引用,相当于取这个数组的第一个元素(一个block的指针),这个就是我们下一个freeblock了?注意这里的block实际上是一个8位整型的别名。这里很是疑惑,于是转过头去看pool初始化的地方的代码。

        init_pool:
            /* Frontlink to used pools. */
            next = usedpools[size + size]; /* == prev */
            pool->nextpool = next;
            pool->prevpool = next;
            next->nextpool = pool;
            next->prevpool = pool;
            pool->ref.count = 1;
            if (pool->szidx == size) {
                /* Luckily, this pool last contained blocks
                 * of the same size class, so its header
                 * and free list are already initialized.
                 */
                bp = pool->freeblock;
                assert(bp != NULL);
                pool->freeblock = *(block **)bp;
                UNLOCK();
                if (use_calloc)
                    memset(bp, 0, nbytes);
                return (void *)bp;
            }
            /*
             * Initialize the pool header, set up the free list to
             * contain just the second block, and return the first
             * block.
             */
            pool->szidx = size;
            size = INDEX2SIZE(size);
            bp = (block *)pool + POOL_OVERHEAD;
            pool->nextoffset = POOL_OVERHEAD + (size << 1);
            pool->maxnextoffset = POOL_SIZE - size;
            pool->freeblock = bp + size;
            *(block **)(pool->freeblock) = NULL; //----#1
            UNLOCK();
            if (use_calloc)
                memset(bp, 0, nbytes);
            return (void *)bp;

在此看到代码注释1的时候,就明白了,初始化的时候,就使用了freeblock的第一个block指针,也就是说每个block的第一个指针大小(在32位机上就是第一个32位,在64位机上就是第一个64位)是用来存放下一个block的内存地址的。注意这里最开始并没有把整个pool都分割完,只是用了最前面两个block,然后freeblock以NULL结尾。那么当我们将要分配第三块block时,会发生什么呢?

            if (pool->nextoffset <= pool->maxnextoffset) {
                /* There is room for another block. */
                pool->freeblock = (block*)pool +
                                  pool->nextoffset;
                pool->nextoffset += INDEX2SIZE(size);
                *(block **)(pool->freeblock) = NULL;
                UNLOCK();
                if (use_calloc)
                    memset(bp, 0, nbytes);
                return (void *)bp;
            }

这里的pool实际上就上面我说的pool_header,通过它里面nextoffset的协助,我们继续划出了一块block,同时将freeblock的next置为NULL。

这里就得到证实,在分配block的时候,的确是使用了block的第一个32/64位做了连接这个链表的线索。当然如果想进一步证实,可以自己编译一个python的debug版本,调试一下看看,这里我就先不做了。(哎,又偷懒了)

剩下的问题

  1. 上面只是介绍了分配和释放对象大小小于512字节的情况,但是大于512字节的要怎么维护的呢?
  2. 比如一个list对象,刚开始的时候是比较小的,但是随着计算的增加,它是有可能超过512字节的,那么大小超过后会怎么处理呢?

更新

今天看到这篇文章,里面说到二维数组跟二级指针的问题,跟这里的二级指针的使用可以对照一下。

为什么不能将二维数组名传递给二级指针?

假如我们将a赋值给p,p = (int**)a; 既然p是二级指针,那么当然可以这么用:**p; 这样会出什么问题呢?
1)首先看一下p的值,p指向a[0][0],即p的值为a[0][0]的地址;
2)再看一下p的值,p所指向的类型是int,占4字节,根据前面所讲的解引用操作符的过程:从p指向的地址开始,取连续4个字节的内容。得到的正式a[0][0]的值,即0。
3)再看一下**p的值,诶,报错了?当然报错了,因为你访问了地址为0的空间,而这个空间你是没有权限访问的。

备注

如果图片看不清楚的话,我放到了网盘里面,可以去那里看。

https://pan.baidu.com/s/1i50XHOD

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值