Python整数对象池:“内存泄漏”?

“墙上的斑点”

我第一次注意到短裤上的那个破洞,大概是在金年的三月上旬。若是想要知道具体的时间,那就得回想一下当时我看见的东西。我还可以回忆起,游泳池顶上,摇曳的、白色的灯光不停地映在个人短裤上;有三五名少年一同扎进了水里。哦,那是大概是冬天,由于我回忆起当时的前一天我和室友吃了部队锅,那段时间我没有吸烟,反而嚼了许多口香糖,糖纸老是掉下去,无心中埋下头,这是我第一次看到短裤上的那个破洞。html


今天在机场等shuttle时,听到旁边的两个年轻人精神焕发地讨论游泳的话题。莫名地回想起来,几年前看了一篇讲述Python内部整数对象管理机制的文章,其中谈到了Python应用内存池机制来对“大”整数对象进行管理。从它出发,我想到了一些问题,想要在这里讨论一下。python

背景

注:本文讨论的Python版本是Python 2 (2.7.11),C实现。数组

“一切皆对象”

咱们知道,Python的对象,本质上是C中的结构体(生存于在系统堆上)。全部Python对象的根源都是PyObject这个结构体。缓存

打开Python源码目录的Include/,能够找到object.h这一文件,这一个文件,是整个Python对象机制的基础。搜索PyObject,咱们将会找到:app

typedef struct _object {
    PyObject_HEAD
} PyObject;

再看看PyObject_HEAD这个宏:函数

#define PyObject_HEAD            \
    _PyObject_HEAD_EXTRA        \
    Py_ssize_t ob_refcnt;        \
    struct _typeobject *ob_type;

在实际编译出的PyObject中,有ob_refcnt这个变量和ob_type这个指针。前者用于Python的引用计数垃圾收集,后者用于指定这个对象的“类型对象”。Python中能够把对象分为“普通”对象和类型对象。也就是说,表示对象的类型,是经过一个指针来指向另外一个对象,即类型对象,来实现的。这是“一切皆对象”的一个关键体现。性能

Python中的整数对象

Python里面,整数对象的头文件intobject.h,也能够在Include/目录里找到,这一文件定义了PyIntObject这一结构体做为Python中的整数对象:学习

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

上面提过了,每个Python对象的ob_type都指向一个类型对象,这里PyIntObject则指向PyInt_Type。想要了解PyInt_Type的相关信息,咱们能够打开intobject.c,并找到以下内容:this

PyTypeObject PyInt_Type = {
    PyObject_HEAD_INIT(&PyType_Type)
    0,
    "int",
    sizeof(PyIntObject),
    0,
    (destructor)int_dealloc,        /* tp_dealloc */
    (printfunc)int_print,            /* tp_print */
    0,                    /* tp_getattr */
    0,                    /* tp_setattr */
    (cmpfunc)int_compare,            /* tp_compare */
    (reprfunc)int_repr,            /* tp_repr */
    &int_as_number,                /* tp_as_number */
    0,                    /* tp_as_sequence */
    0,                    /* tp_as_mapping */
    (hashfunc)int_hash,            /* tp_hash */
        0,                    /* tp_call */
        (reprfunc)int_repr,            /* tp_str */
    PyObject_GenericGetAttr,        /* tp_getattro */
    0,                    /* tp_setattro */
    0,                    /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES |
        Py_TPFLAGS_BASETYPE,        /* tp_flags */
    int_doc,                /* tp_doc */
    0,                    /* tp_traverse */
    0,                    /* tp_clear */
    0,                    /* tp_richcompare */
    0,                    /* tp_weaklistoffset */
    0,                    /* tp_iter */
    0,                    /* tp_iternext */
    int_methods,                /* tp_methods */
    0,                    /* tp_members */
    0,                    /* tp_getset */
    0,                    /* tp_base */
    0,                    /* tp_dict */
    0,                    /* tp_descr_get */
    0,                    /* tp_descr_set */
    0,                    /* tp_dictoffset */
    0,                    /* tp_init */
    0,                    /* tp_alloc */
    int_new,                /* tp_new */
    (freefunc)int_free,                   /* tp_free */
};

这里给Python的整数类型定义了许多的操做。拿int_dealloc,int_freeint_new这几个操做举例。显而易见,int_dealloc负责析构,int_free负责释放该对象所占用的内存,int_new负责建立新的对象。int_as_number也是比较有意思的一个field。它指向一个PyNumberMethods结构体。PyNumberMethods含有许多个函数指针,用以定义对数字的操做,好比加减乘除等等。spa

通用整数对象池

Python里面,对象的建立通常是经过Python的C API或者是其类型对象。这里就不详述具体的建立机制,具体内容能够参考Python的有关文档。这里咱们想要关注的是,整数对象是如何存活在系统内存中的。

整数对象大概会是常见Python程序中使用最频繁的对象了。而且,正如上面提到过的,Python的一切皆对象并且对象都生存在系统的堆上,整数对象固然不例外,那么以整数对象的使用频度,系统堆将面临不可思议的高频的访问。一些简单的循环和计算,都会导致malloc和free一次次被调用,由此带来的开销是难以计数的。此外,heap也会有不少的fragmentation的状况,进一步致使性能降低。

这也是为何通用整数对象池机制在Python中获得了应用。这里须要说明的是,“小”的整数对象,将所有直接放置于内存中。怎么样定义“小”呢?继续看intobject.c,咱们能够看到:

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* References to small integers are saved in this array so that they
   can be shared.
   The integers that are saved are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

值在这个范围内的整数对象将被直接换存在内存中,small_ints负责保存它们的指针。能够理解,这个数组越大,使用整数对象的性能(极可能)就越高。可是这里也是一个trade-off,毕竟系统内存大小有限,直接缓存的小整数数量太多也会影响总体效率。

与小整数相对的是“大”整数对象,也就是除开小整数对象以外的其余整数对象。既然不可能再缓存全部,或者说大部分经常使用范围的整数,那么一个妥协的办法就是提供一片空间让这些大整数对象按需依次使用。Python也正是这么作的。它维护了两个单向链表block_listfree_list。前者保存了许多被称为PyIntBlock的结构,用于存储被使用的大整数的PyIntObject;后者则用于维护前者全部block之中的空闲内存。

仍旧是在intobject.c之中,咱们能够看到:

struct _intblock {
    struct _intblock *next;
    PyIntObject objects[N_INTOBJECTS];
};

typedef struct _intblock PyIntBlock;

一个PyIntBlock保存N_INTOBJECTS个PyIntObject。

如今咱们来思考一下一个Python整数对象在内存中的“一辈子”。

被建立出来以前,先检查其值的大小,若是在小整数的范围内,则直接使用小整数池,只用更新其对应整数对象的引用计数就能够了。若是是大整数,则须要先检查free_list看是否有空闲的空间,要是没有则申请新的内存空间,更新block_listfree_list,不然就使用free_list指向的下一个空闲内存位置而且更新free_list

“内存泄漏”?

So far so good. 上述的机制能够很好减轻fragmentation的问题,同时能够根据所跑的程序不一样的特色来作fine tuning从而编译出本身认为合适的Python。可是咱们只说了Python整数对象的“来”尚未提它的“去”。当一个整数对象的引用计数变成0之后,会发生什么事情呢?

小整数对象自是没必要担忧,始终都是在内存中的;大整数对象则须要调用析构操做,int_deallocintobject.c):

static void
int_dealloc(PyIntObject *v)
{
    if (PyInt_CheckExact(v)) {
        Py_TYPE(v) = (struct _typeobject *)free_list;
        free_list = v;
    }
    else
        Py_TYPE(v)->tp_free((PyObject *)v);
}

这个PyInt_CheckExact,来自于intobject.h

#define PyInt_CheckExact(op) ((op)->ob_type == &PyInt_Type)

它起到了类型检查的做用。因此若是这个指针v指向的不是Python原生整数对象,则int_dealloc直接调用该类型的tp_free操做;不然把再也不须要的那块内存放入free_list之中。

Py_TYPE的定义:

#ifndef Py_TYPE
    #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
#endif

能够看出,free_list所维护的单向链表,是使用ob_type这个field来连接先后元素的。

这也就是说,当一个大整数PyIntObject的生命周期结束时,它以前的内存不会交换给系统堆,而是经过free_list继续被该Python进程占有。

假若一个程序使用不少的大整数呢?假若每一个大整数只被使用一次呢?是否是很像内存泄漏?

咱们来作个简单的计算,假如你的电脑是Macbook Air,8GB Memory,若是你的PyIntObject占用24个Byte,那么满打满算,可以存下大约357913941个整数对象。

下面作个实验。如下程序运行在Macbook Pro (mid 2015), 2.5Ghz i7, 16 GB Memory,Python 2.7.11的环境下:

l = list()
num = 178956971

for i in range(0, num):
    l.append(i)
    if len(l) % 100000 == 0:
        l[:] = []

运行这个程序,会发现它占用了5.44GB的内存:
5.44GB

若是把整数个数减半,好比使用89478486,则会占用2.72GB内存(正好原来一半):
2.72GB

一个PyIntObject占用多大内存呢?
图片描述

讲道理,24 bytes x 178956971 = 4294967304 bytes,约等于2^32,也就是4GB,那么为何会占用5.44GB呢?

这并不是程序其余部分的overhead,由于,就算你的程序只含有:

for i in range(0, 178956971):
    pass

它仍旧会占用5.44GB内存。5.44 x 2^30 / 178956971大约等于32.64,也就是均摊下来一个整数对象占用了32.64个Byte.

这个问题能够做为一个简单的思考题,这里就不讨论了。

总结

Python的整数对象管理机制并不复杂,但也有趣,刚接触Python的时候是很好的学习材料。细纠下来会发现有不少工程上的考虑以及与之相关的现象,值得咱们深刻挖掘。

相关文章
相关标签/搜索