python底层设计:整数对象设计

目录

 

1.初识整数

2.PyIntObject创建与维护

2.1 对象的创建途径

2.2小整数对象

2.3大整数对象

2.4 添加和删除


1.初识整数

    python中整数是通过PyIntObject对象实现的,它属于imutable(不可变对象),也就是说在创建一个PyIntObject对象后,就再也不能改变该对象的值了;另外这里的int并非真正的int,而是long 类型,我们看下它的结构源码可以知道:

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

    在python程序中,整数使用是非常广泛的,频繁的创建和释放内存是非常影响执行效率的。假如直接使用引用计数机制,执行下面这个循环的话。

>>> for i in range(10000)

    这个for循环需要10000次申请和释放内存,这意味着堆需要面临整数对象狂风暴雨的访问,这样的执行效率肯定是无法接受的,那么设计一个高效的机制避免整数对象的使用成为python的瓶颈是至关重要的。Guido给出的解决方案是整数对象池:小整数对象池和通用整数对象池。整数对象池是本章的重点。

    在讲解整数对象池之前我们先看看PyIntObject对象的元信息:

PyTypeObject PyInt_Type = {
	PyObject_HEAD_INIT(&PyType_Type)
	"int",
	sizeof(PyIntObject),
	(destructor)int_dealloc,		/* tp_dealloc */
	(printfunc)int_print,			/* tp_print */

	(cmpfunc)int_compare,			/* tp_compare */
	(reprfunc)int_repr,			/* tp_repr */
	&int_as_number,				/* tp_as_number */

	(hashfunc)int_hash,			/* tp_hash */
    (reprfunc)int_repr,			/* tp_str */
	PyObject_GenericGetAttr,		/* tp_getattro */
	Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES |
		Py_TPFLAGS_BASETYPE,		/* tp_flags */
	int_doc,				/* tp_doc */
	int_methods,				/* tp_methods */
	int_new,				/* tp_new */
	(freefunc)int_free,           		/* tp_free */
    ......
};

   从上面可以知道元信息包含了PyIntObject的应该占用的内存大小,对象文档信息,还有PyIntObject支持的操作,部分操作功能如下表:

int_dealloc 析构操作
int_print 打印操作
int_compare 比较操作
int_hash 获得hash值
int_as_number 数值操作集合
int_repr 转化为PyStringObject对象
int_free 释放操作
int_methods 成员函数集合

    我先分析下python中compare函数的实现,源码如下:

static int
int_compare(PyIntObject *v, PyIntObject *w)
{
	register long i = v->ob_ival;
	register long j = w->ob_ival;
	return (i < j) ? -1 : (i > j) ? 1 : 0;
}

   从源码可以看出,对比操作实际上就是简单的将它所维护的long值进行对比。这里还要介绍下int_methods 这个域,它包含了几十种数值操作,如下:

static PyNumberMethods int_as_number = {
	(binaryfunc)int_add,	/*nb_add*/
	(binaryfunc)int_sub,	/*nb_subtract*/
	(binaryfunc)int_mul,	/*nb_multiply*/
	(binaryfunc)int_classic_div, /*nb_divide*/
	(binaryfunc)int_mod,	/*nb_remainder*/
	.....
};

   这里给大家讲解其中的add操作,我们先看看它的源码:

#define PyInt_AS_LONG(op) (((PyIntObject *)(op))->ob_ival)

#define CONVERT_TO_LONG(obj, lng)		\
	if (PyInt_Check(obj)) {			\
		lng = PyInt_AS_LONG(obj);	\
	}					\
	else {					\
		Py_INCREF(Py_NotImplemented);	\
		return Py_NotImplemented;	\
	}
static PyObject *int_add(PyIntObject *v, PyIntObject *w)
{
	register long a, b, x;
	CONVERT_TO_LONG(v, a);
	CONVERT_TO_LONG(w, b);
	x = a + b;
	if ((x^a) >= 0 || (x^b) >= 0)
		return PyInt_FromLong(x);
	return PyLong_Type.tp_as_number->nb_add((PyObject *)v, (PyObject *)w);
}

  宏PyInt_AS_LONG的意义是可以省去一次函数调用的开销,但是牺牲了类型安全。我们可以从源码看出,整数对象确实是imutable,两数相加操作完成后,参与的对象都没有发生改变,取而代之的是一个全新的PyIntObject。如果相加后发生来溢出,则返回PyLongObject对象。

 

2.PyIntObject创建与维护

 

2.1 对象的创建途径

   源码里提供了5种创建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);

  分别从字符串,long,unicode,Py_ssize_t,size这五种类型进行转换,其中前三种是最常见的。为了深刻理解PyIntObject的创建过程,我们必须了解python中整数对象在内存的组织方式。

 

2.2小整数对象

  在编程中,有些小数值使用非常频繁,不可能每次都要重新创建,这样太浪费时间了,Guido使用了一个对象池,将所有小数值对象都丢在池里面,池里的任意对象都可以被共享。那有一个问题,多小的数值属于小整数,分界点在哪里??

  python提供了一种方法来修改边界,但是需要修改源码,大小整数边界定义源码如下:

#define NSMALLPOSINTS		257
#define NSMALLNEGINTS		5
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* 对小整数的引用保存在这个数组中,以便它们可以共享。
保存的整数是从NSMALLNEGINTS(包含)到NSMALLPOSINTS(不包含)的整数。
*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

  这里将小整数集合范围设置为[-5,257),我们可以通过NSMALLNEGINTS和NSMALLPOSINTS修改小整数边界。然后根据边界将小整数缓存到内存中直到python程序结束为止。其中指针small_ints指向小整数内存块。

2.3大整数对象

  谁敢保证只有小整数才会被频繁使用,但是把所有数值对象都缓存到内存的话又会被别人笑话。于是乎python设计者们作出的妥协,对于小整数,完全地缓存在小整数对象池,对于其他整数来说,python运行环境将提供一块内存空间,这些内存空间由这些大整数轮流使用,也就是说,谁需要的时候谁就使用。下面将详细剖析其实现机制。

  在python中,有一个PyIntBlock结构,在这个结构上,实现了一个单向列表。

#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_SIZE - BHEAD_SIZE)大小的内存(block)空间,PyIntBlock的单向列表通过block_list来维护,每个block都拥有一个objects,objects用于存储被缓存的对象的内存。如图:

python底层设计:整数对象设计_第1张图片

python底层设计:整数对象设计_第2张图片

 

  我们可以想象,block中的objects有些被使用了,而有些还处于空闲状态,这些空闲内存必须组织起来,这样,python在需要新内存来存储新对象时才能快速获取所需内存,python使用来一个单向链free_list来维护,free_list就是自由内存链表的表头,如果还有空闲空间,新增的对象存储在free_list指向的位置。当然,最开始的时候这两个指针肯定是null。

 

python底层设计:整数对象设计_第3张图片

 

 

 

2.4 添加和删除

  现在我们大体可以想象python整数对象系统在内存是一种怎样的结构了。下面通过PyInt_FromLong来展示PyIntObject是如何从无到有地产生的。 

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);
		return (PyObject *) v;
	}
    为通用整数对象池申请新空间
	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;
}

PyIntObject对象的创建通过2个步骤完成:

  • 如果小对象池机制被激活,则尝试使用小整数对象池;
  • 如果不能使用小整数对象池,则使用通用的整数对象池。

 

  python 是如何创建通用整数对象池的呢?我们看看源码

static PyIntObject *fill_free_list(void)
{
	PyIntObject *p, *q;
    **首先申请内存空间,并链接到已有的block_list中
	p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));
	if (p == NULL)
		return (PyIntObject *) PyErr_NoMemory();
	((PyIntBlock *)p)->next = block_list;
	block_list = (PyIntBlock *)p;
	**然后将PyIntBlock中的objects数组转变成单向链表
	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;
}

执行流程:

  • 申请内存空间,并链接到已有的block_list中;
  • 将PyIntBlock中的objects数组转变成单向链表;

转换完成之后的block如下图:

python底层设计:整数对象设计_第4张图片


 

举个例子:创建2,3,4这样的整数对象,使用的是小整数对象池,创建过程如下图所示

python底层设计:整数对象设计_第5张图片

 

  空闲内存互连是在int_dealloc被调用时实现的,如图:

python底层设计:整数对象设计_第6张图片

 

  在新增4的时候,发现还有空闲空间,此时将4存储在free_list指向的为止,当释放3的内存后,free_list指向3的位置。

   需要注意的是:fill_free_list不仅发生在PyInt_FromLong首次调用,在block的空闲内存被使用完时(free_list变为null)也会调用。

你可能感兴趣的:(Python,python,源码,整数,原理)