[绝对原创 转载请注明出处]
Python源码剖析
——整数对象PyIntObject(2)
本文作者: Robert Chen ([email protected])
在intobject.h中可以看到,可以从三种途径获得一个PyIntObject对象:
PyObject *PyInt_FromLong(long ival)
PyObject* PyInt_FromString(char *s, char **pend, int base)
#ifdef Py_USING_UNICODE
PyObject*PyInt_FromUnicode(Py_UNICODE *s, int length, int base)
#endif
分别是从long值,从字符串以及Py_UNICODE对象生成PyIntObject对象。在这里我们只考察从long值以及字符串生成PyIntObject对象。因为PyInt_FromString实际上是先将字符串转换成浮点数,然后再调用PyInt_FromFloat:
[intobject.c]
PyObject* PyInt_FromString(char *s, char **pend, int base)
{
char *end;
long x;
......
//convert string to long
if (base == 0 && s[0] == '0')
{
x = (long) PyOS_strtoul(s, &end, base);
}
else
x = PyOS_strtol(s, &end, base);
......
return PyInt_FromLong(x);
}
为了理解整数对象的创建过程,必须要深入了解Python中整数对象在内存中的组织方式,实际上,一个个的整数对象在内存中并不是独立存在,单兵作战的,而是形成了一个整数对象体系。我们首先就重点考察一下Python中的整数对象体系结构。
在实际的编程中,对于数值比较小的整数,比如1,2,29等等,可能在程序中会非常频繁地使用。想一想C语言中的for循环,就可以了解这些小整数会有多么频繁的使用场合。在Python中,所有的对象都是存活在系统堆上。这就是说,如果没有特殊机制,对于这些频繁使用的小整数对象,Python将一次又一次地在使用malloc在堆上申请空间,并不厌其烦地一次次free。这样的操作不仅大大降低了运行效率(Python本来就以速度慢被人诟病了 :),而且会在系统堆上造成内存碎片,严重影响Python的整体性能。
显然,Guido是决不能容许这样的方案存在的,于是在Python中,对小整数对象,使用了对象池技术。刚才我们说了,PyIntObject对象是Immutable对象,这带来了一个天大的喜讯,所以对象池里的PyIntObject对象能够被任意地共享。
给你一个整数100,你说它是个“小”整数吗?那么101呢?小整数和大整数的分界线在哪里?Python的回答是“it’s all up on you”,你想它在哪里它就在哪里。
Python中提供了一种方法,通过这种方法,用户可以动态地调整小整数与大整数的分界线,从而动态确定对象池中到底应该有多少个对象。呃,但是,老实说,Python提供的这种方法比较原始,为了达到动态调整的目的,你只有自己修改源代码。
[intobject.c]
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 100
#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];
#endif
Python2.4中,将小整数集合的范围默认设定为[-5,100)。但是你完全可以修改NSMALLPOSINTS和NSMALLNEGINTS,重新编译Python,从而将这个范围向两端伸展或收缩。
对于小整数对象,Python直接将这些整数对应的PyIntObject缓存在内存中。那么对于大整数呢?很显然,整数对象是编程中使用得非常多的东西,谁敢保证只有小整数才会被频繁地使用呢。如果将所有的整数对应的PyIntObject对象都缓存在内存池中,自然是再理想不过了,但是这样对内存的使用是会被视为败家子,难免遭人鄙视的:)。时间与空间的两难选择,这个计算机领域最基本的矛盾在这里浮出水面。
Python的设计者们所做出的妥协是,对于小整数,完全地缓存其PyIntObject对象。而对其它整数,Python运行环境将提供一块内存空间,这些内存空间由这些大整数轮流使用,也就是说,谁需要的时候谁就使用。这样免去了不断地malloc之苦,又在一定程度上考虑了效率问题。我们下面将详细剖析其实现机制。
在Python中,有一个PyIntBlock结构,在这个结构的基础上,实现了的一个单向列表。
[intobject.c]
#define BLOCK_SIZE 1000 /* 1K less typical malloc overhead */
#define BHEAD_SIZE 8 /* Enough for a 64-bit pointer */
#define N_INTOBJECTS ((BLOCK_SIZE - BHEAD_SIZE) / sizeof(PyIntObject))
struct _intblock {
struct _intblock *next;
PyIntObject objects[N_INTOBJECTS];
};
typedef struct _intblock PyIntBlock;
static PyIntBlock *block_list = NULL;
static PyIntObject *free_list = NULL;
PyIntBlock,顾名思义,就是说这个结构里维护了一块(block)内存,其中保存了一些PyIntObject对象。从PyIntBlock的声明中可以看到,在一个PyIntBlock中维护着N_INTOBJECTS对象,做一个简单的计算,就可以知道是82个。显然,这个地方也是Python的设计者留给你的可以动态调整地地方,不过,你需要再一次地修改源代码并重新编译。
PyIntBlock的单向列表通过block_list维护,而这些block中的PyIntObject的列表中可以被使用的内存通过free_list来维护。最开始的时候,这两个指针都被设置为空指针,如图3所示。
(注意:此后,我们将用红色箭头表示free_list,蓝色箭头表示block_list)
好了,现在我们大体上了解了Python中整数对象在内存中的体系结构。下面通过对PyInt_FromLong的考察,真实地展现一个个PyIntObject对象是如何从无到有地产生并融入到Python的整数对象体系中的。
[intobject.c]
PyObject* PyInt_FromLong(long ival)
{
register PyIntObject *v;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
v = small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
#ifdef COUNT_ALLOCS
if (ival >= 0)
quick_int_allocs++;
else
quick_neg_int_allocs++;
#endif
return (PyObject *) v;
}
#endif
if (free_list == NULL) {
if ((free_list = fill_free_list()) == NULL)
return NULL;
}
/* Inline PyObject_New */
v = free_list;
free_list = (PyIntObject *)v->ob_type;
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
return (PyObject *) v;
}
在调用PyInt_FromLong时,首先会检查传入的long值是否属于小整数的范围,如果确实是属于小整数,那么很简单了,只需要返回在对象池中的对应的对象就可以了。
如果传入的long值不是属于小整数,Python就会转向由block_list维护的内存。当首次调用PyInt_FromLong时,因为free_list为NULL,这时会调用fill_free_list:
[intobject.c]
static PyIntObject* fill_free_list(void)
{
PyIntObject *p, *q;
/* Python's object allocator isn't appropriate for large blocks. */
p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));
if (p == NULL)
return (PyIntObject *) PyErr_NoMemory();
((PyIntBlock *)p)->next = block_list;
block_list = (PyIntBlock *)p;
/* Link the int objects together, from rear to front, then return
the address of the last int object in the block. */
p = &((PyIntBlock *)p)->objects[0];
q = p + N_INTOBJECTS;
while (--q > p)
q->ob_type = (struct _typeobject *)(q-1);
q->ob_type = NULL;
return p + N_INTOBJECTS - 1;
}
在fill_free_list中,会申请一个PyIntBlock结构体的对象,如图4所示:
(注意:图中的虚线并不表示指针关系,虚线表示objects的更详细地表示方式,下同)
然后,会将该Block中的PyIntObject数组中的所有的PyIntObject对象通过指针依次连接起来,就像一个单向链表一样。在这里,使用了PyObject中的ob_type指针作为连接指针。可以看到,Python的设计者为了解决问题,也就不再考虑什么类型安全了。就像政治一样,计算机也是一门妥协的艺术 :)。fill_free_list完成后最终的Block如图5所示。可以看到,这时候,free_list也在它该出现的位置了。从free_list开始,沿着ob_type指针,就可以遍历刚刚创建的PyIntBlock对象中的所有可使用的为PyIntObject准备的内存了,如图5所示:
当一个Block中还有剩余的内存没有被一个PyIntObject占用时,free_list就不会指向NULL。所以在这种情况下调用PyInt_FromLong不会申请新的Block。只有在一个Block中的内存都被占用了,PyInt_FromLong才会再次调用fill_free_list申请新的空间,为新的PyIntObject创造新的家园。图6展示了两次申请Block后Block链表的情况。值得注意的是,block_list始终是指向最新创建的PyIntBlock对象。
在PyInt_FromLong中,当必要的空间被申请之后,将会把当前可用的Block中的内存空间划出一块,将在这块内存上创建我们需要的PyIntObject对象,同时,还会调整完成必要的初始化工作,以及调整free_list指针,使其指向下一块还没有被占用的内存。
从图中我们发现,两个PyIntBlock处于同一个链表当中,但是每一个PyIntBlock中至关重要的存放PyIntObject对象的objects却是分离的。这样的结构存在着隐患,考虑这样的情况:
现在有两个PyIntBlock对象,PyIntBlock1和PyIntBlock2,PyIntBlock1中的objects已经被PyIntObject对象填满,而PyIntBlock2种的object只填充了一部分。所以现在free_list指针指向的是PyIntBlock2.objects中空闲得内存块。假设现在PyIntBlock1.objects中的一个PyIntObject对象被删除了,现在PyIntBlock1中有空闲的内存可用了,那么下次创建新的PyIntObject对象时应该使用PyIntBlock1中的这块内存。那么如何使Python意识到这块重获自由的内存呢?如果象上图所示的PyIntBlock对象间的objects没有任何联系,那么显然不可能实现这样的功能,所以它们之间一定存在联系。实际上,不同PyIntBlock对象之间的空闲内存块是被链接在一起的,形成了一个单向链表,表头就是free_list。
那么,不同PyIntBlock中的空闲内存块是在什么时候被链接在一起的呢,这一切都发生在一个PyIntObject对象被销毁的时候。
列位看官,花开两朵,各表一支:)这里我们先放下自由内存链表,仔细考察一下一个PyIntObject对象在被销毁时都发生了什么事。在对Python中对象机制的分析中,我们已经看到,每一个对象都有一个引用计数与之相连,当这个引用计数减少到0时,就意味着这个世上再也没有谁需要它了,于是Python运行时会负责将这个对象销毁。Python中不同对象在销毁时会进行不同的动作,销毁动作在与对象对应的类型对象中被定义,这个关键的操作就是类型中的tp_dealloc。
下面看一看PyIntObject对象的tp_dealloc操作:
[intobject.c]
static void int_dealloc(PyIntObject *v)
{
if (PyInt_CheckExact(v)) {
v->ob_type = (struct _typeobject *)free_list;
free_list = v;
}
else
v->ob_type->tp_free((PyObject *)v);
}
在前面我们说了,由block_list维护的PyIntBlock的列表中的内存实际是所有的大整数对象所共同分享的。俗话说,皇帝轮流坐,明年到我家。当一个PyIntObject对象被删除时,它所占有的内存并没有被释放,归还给系统,而是继续被保留着。但是这一块内存现在已经是归free_list所维护的链表所有了,这表明在PyIntObject对象被删除后,它所占用的内存成了一块自由内存,可以供别的PyIntObject使用了。int_dealloc完成的就是这么一个简单的指针维护的工作。当然,这些动作是在删除的对象确实是一个PyIntObject对象时发生的。如果删除的对象是一个整数的派生类的对象,那么int_dealloc不做任何动作,只是简单地调用派生类型中制定的释放函数。
在图7中我们形象地展示相继创建和删除PyIntObject对象,在这一过程中,内存中的PyIntObject对象以及free_list指针的变化情况。同时我们还展示了small_ints这个小整数的对象池。
现在回头来看看刚才提到的不同PyIntBlock对象的objects间的空闲内存的互连问题。其实很简单,不同PyIntBlock对象中空闲内存的互连也是在int_dealloc被调用时实现的。图8展示了这个过程(黄色表示空闲内存)。
现在,关于Python的整数对象体系,我们只剩下最后一个问题了。在small_ints中,我们看到,它维护的只是PyIntObject的指针,那么这些与天地同寿的小整数对象是在什么地方被创建和初始化的呢。完成这一切的神秘的函数正是_PyInt_Init。
[intobject.c]
int _PyInt_Init(void)
{
PyIntObject *v;
int ival;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++)
{
if (!free_list && (free_list = fill_free_list()) == NULL)
return 0;
/* PyObject_New is inlined */
v = free_list;
free_list = (PyIntObject *)v->ob_type;
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
small_ints[ival + NSMALLNEGINTS] = v;
}
#endif
return 1;
}
这些永生不灭的小整数对象也是生存在由block_list所维护的内存上,在Python运行时初始化的时候,_PyInt_Init被调用,内存被申请,小整数对象被创建,然后就仙福永享,寿与天齐了 :)