Python内部关于int对象的实现在我之前的两篇文章中其实已经简单的介绍过,本文会前面的基础上更加深入的分析int对象的内部实现,以及Python对int对象进行优化而采用的缓存技术等等.首先还是来看看int对象在C层面的一个数据结构吧.
typedef struct {
PyObject_HEAD
long ob_ival;
} PyIntObject;
Include/intobject.h
PyObject_HEAD是一个宏,这在前面的几篇文章中都详细分析过,所以对于int对象来说最重要的莫过于ob_ival
成员了,这个成员用来存放实际存储的数据.有了这个基础我们就可以在Python源码中开始漫游了.先来看看int对象做了哪些初始化动作吧.
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 *)Py_TYPE(v);
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
small_ints[ival + NSMALLNEGINTS] = v;
}
#endif
return 1;
}
Object/intobject.c
这段初始化代码信息量很大,包含了本文即将分析的小整数对象的缓存优化,还有大整数对象池等知识点.这段初始化代码做了两件事,第一件事就是创建大整数对象池,第二件事就是小整数对象的缓存的初始化.具体细节后面分析到相关知识点的时候再具体分析.这里我们重点看一下PyObject_INIT
,这是一个宏
专门用于初始化类型对象的.在上面的初始化代码中v是一个PyIntObject
类型,但是还没有进行初始化.通过PyObject_INIT
宏.将PyIntObject
对象和这个对象对应的类型PyInt_Type
关联起来.这也是我在前面几篇文章中提到的,对象会在初始化的时候固定其类型.PyInt_Type
是一个专门为int对象进行定制的一个PyTypeObject(如果你忘了这个对象是干什么的,请看我之前的文章)对象.
下面是PyObject_INIT
宏的具体实现:
#define PyObject_INIT(op, typeobj) \
( Py_TYPE(op) = (typeobj), _Py_NewReference((PyObject *)(op)), (op) )
Include/objimpl.h
#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
Include/object.h
PyObject_INIT
的实现很简单,就是把PyInt_Type
和PyIntObject
的ob_type
成员关联起来.这样就完成了PyIntObject
对象的初始化了,然后再增加引用计数.到此初始化就结束了,说完了初始化,该说说如何创建一个int对象了吧.
看一下相关的头文件,发现Python居然提供了这么多的构造函数,可以从字符串,UNICODE,long,size_t,Py_ssize_t等类型来构造一个PyIntObject对象.
PyAPI_FUNC(PyObject *) PyInt_FromString(char*, char**, int);
#ifdef Py_USING_UNICODE
PyAPI_FUNC(PyObject *) PyInt_FromUnicode(Py_UNICODE*, Py_ssize_t, int);
#endif
PyAPI_FUNC(PyObject *) PyInt_FromLong(long);
PyAPI_FUNC(PyObject *) PyInt_FromSize_t(size_t);
PyAPI_FUNC(PyObject *) PyInt_FromSsize_t(Py_ssize_t);
Include/object.h
当你开始去分析每一个构造函数的时候,你会发现另外一个有意思的地方,那就是这些构造函数最终都会去调用PyInt_FromLong
这个函数,构造函数大同小异因此接下来我们通过分析PyInt_FromLong
函数的实现来学习如何构造int对象.
PyObject *
PyInt_FromLong(long ival)
{
register PyIntObject *v;
//Setp1: 查看ival是否在整数范围内,如果在就直接返回缓存的PyIntObject对象
#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
//Setp2: 查看是否有空闲的PyIntObject对象,如果没有调用fill_free_list开始分配一个
if (free_list == NULL) {
if ((free_list = fill_free_list()) == NULL)
return NULL;
}
//Setp3: 初始化PyIntObject,然后赋值,最后返回.
/* Inline PyObject_New */
v = free_list;
free_list = (PyIntObject *)Py_TYPE(v);
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
return (PyObject *) v;
}
Object/intobject.c
上面的代码分成了几部分,第一部分就是查看传入的ival是否在小整数的范围内,如果在直接返回缓存的PyIntObject对象,如果不存在就去查看整数对象池中是否有PyIntObject对象如果有则从对象池链表中摘下来返回,如果没有则分配一个,然后初始化,赋值最后返回.但是这段代码远比我说的要复杂多了,下文会详细的介绍.到此为止,一个int对象做了哪些初始化,以及如何构造一个int对象都已经介绍完毕了.我相信大家此时心里至少有了一个全局观,但是对细节的把握肯定还是不够的,上文中提到的小整数对象缓存,大整数对象池都是存疑的.那么下面我将带领大家更加深入的解析Python中的int对象.
大家先来看一段python程序,通过这段python程序可以让我们直观的看到什么是小整数对象缓存机制.
>>> a = 1
>>> b = 1
>>> id(a)
35664216
>>> id(b)
35664216
>>> a = 10000
>>> b = 10000
>>> id(a)
35946744
>>> id(b)
35946624
id命令是用于打印一个对象在内存的虚拟地址的,通过上面的代码你会发现当a和b是1的时候其对象地址是相同的,也就是说两者其实是一个对象,只是名字不同而已,当a和b是10000的时候你又会发现两个对象的地址不同的了,也就是说两者是两个不同的对象.你还可以接着测试a和b为2,3,4…的时候,试图找一找临界点,在什么范围情况下是相同的,什么范围情况下是不同的.看到这里是不是对上面的结果很好奇呢?,这里用到的就是所谓的小整数缓存机制,Python默认对于[-5,257)范围内的整数都是使用的小整数缓存机制,也就是说当int对象的值范围在[-5,257)内其实用的都是全局的已经初始化好的int对象.现在我们回到源码中来验证一下吧.还记得上文说的int对象初始化吗?,没错就是哪里
#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 *)Py_TYPE(v);
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
small_ints[ival + NSMALLNEGINTS] = v;
}
#endif
Object/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];
#endif
Object/intobject.c
NSMALLNEGINTS
是5,NSMALLPOSINTS
是257,small_ints
是一个大小是NSMALLNEGINTS + NSMALLPOSINTS
的PyIntObject
类型的数组上面的初始化代码首先是一个for循环,从-NSMALLNEGINTS
遍历到NSMALLPOSINTS
,每次遍历的时候通过 fill_free_list()
创建了一个PyIntObject
对象
然后使用v指向这个刚创建的PyIntObject
对象v = free_list;
,接着初始化这个PyIntObject
对象PyObject_INIT(v, &PyInt_Type);v->ob_ival = ival;
最后用v来初始化全局的对象数组small_ints[ival + NSMALLNEGINTS] = v;
读到这里我相信大家已经对小整数的缓存机制有了一定的了解了吧.现在回头看看int对象构造那个章节结合小整数缓存这个章节,你是不是应该对下面的代码有了更深入的理解了啊.
//Setp1: 查看ival是否在整数范围内,如果在就直接返回缓存的PyIntObject对象
#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
至于这个章节中提到的free_list
,fill_free_list()
等,这是本文即将要讨论的另外一个知识点也就是大整数缓存机制,这里占且不讨论,继续往下看.
从前面的文章中我们已经知道Python中处处都是对象,而对象在C语言层面其实就是在堆上分配的一堆结构体.大量的对象构造和释放会导致大量的内存申请和释放,这势必会导致内存碎片的问题,Python中为了避免内存碎片的问题引入了对象池的,也就是内存释放的时候不再归还给系统,而是放到对象池中.接下来我们来看看Python中对象池是如何实现的吧.
struct _intblock {
struct _intblock *next;
PyIntObject objects[N_INTOBJECTS];
};
typedef struct _intblock PyIntBlock;
一个对象池就是一个PyIntBlock类型的单链表.一个PyIntBlock可以存放N_INTOBJECTS个整数对象.初始的时候这个单链表是空的.
static PyIntBlock *block_list = NULL;
static PyIntObject *free_list = NULL;
block_list指向PyIntBlock单链表中的第一个PyIntBlock,初始的时候这个单链表是空的,那么当需要使用对象池的时候就会调用fill_free_list来创建这个对象池.接下来我们来看看fill_free_list是如何创建的吧.
static PyIntObject *
fill_free_list(void)
{
PyIntObject *p, *q;
/* Python's object allocator isn't appropriate for large blocks. */
//Setp1: 分配一个sizeof(PyIntBlock)大小的内存.
p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));
//处理分配失败的情况
if (p == NULL)
return (PyIntObject *) PyErr_NoMemory();
//Setp2:头插法插入block_list指向的单链表
((PyIntBlock *)p)->next = block_list;
//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;
//setp3: 这一步很巧妙,将PyIntBlock中的objects数组变成了单链表,通过拿前一个PyIntObject对象的ob_type成员指向后一个PyIntObject对象的ob_type成员实现
//这里和linux内核中的单链表实现的思想是如出一辙的.
while (--q > p)
Py_TYPE(q) = (struct _typeobject *)(q-1); //struct _typeobject就是PyTypeObject,Py_TYPE就是取q的ob_type成员
Py_TYPE(q) = NULL;
//setp3: 最后返回PyIntBlock中的objects数组的最后一个成员.
return p + N_INTOBJECTS - 1;
}
通过fill_free_list可以创建PyIntBlock,然后返回其中的objects数组的最后一个元素.那么我们再来看看如何使用PyIntBlock吧.还记得上文中说的int对象初始化和int对象的构造吗?,这两个地方都用到了.
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 *)Py_TYPE(v);
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
small_ints[ival + NSMALLNEGINTS] = v;
}
#endif
return 1;
}
这段初始化代码.不光光初始化了小整数缓存,还创建了一个PyIntBlock,然后使用PyIntBlock中的objects数组来初始化小整数缓存.初始的时候free_list是空,所以所有的for循环中,只有第一次循环的时候才会通过fill_free_list分配一个PyIntBlock对象,然后free_list指向PyIntBlock中的objects数组中的最后一个,接着通过(PyIntObject *)Py_TYPE(v)
指向下一个PyIntObject对象.这里是初始化的时候用到了PyIntBlock,那么我们来看看在构造的时候是如何使用PyIntBlock对象的.
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 *)Py_TYPE(v);
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
return (PyObject *) v;
}
如果是小整数那么直接从小整数缓存中分配一个返回,如果不是那么首先看看free_list是否为空,如果为空说明PyIntBlock已经用完了,那么使用fill_free_list分配一个然后初始化返回.代码还是很容易看懂的.到此为止int对象相关的知识点都已经说完了.