Python源码剖析第二章 整数对象

这章主要是

  • 了解PyIntObject整数对象的整个生命周期,从创建到销毁
  • int对象算是使用频率最为频繁的对象类型了, 因此它的创建,释放性能就变得至关重要了, 在Python中将整数对象划分为大整数对象小整数对象, 这两类在对象的创建和销毁有所不同
  • 了解Python的对象池机制, 如何保证内存的重复利用,避免重复进行开辟释放内存所造成的开销

整数对象的创建流程

在intobject.h中可以看到,为了创建一个PyIntObject对象,Python提供了3条途径

1
2
3
4
5
 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

PyInt_FromString和PyInt_FromUnicode实际上是先将字符串对象和Py_UNICODE对象转成浮点数, 然后调用PyInt_FromLong进行创建, 因此只需了解PyInt_FromLong的代码就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[intobject.c]

PyObject* PyInt_FromLong(long ival)
{
register PyIntObject *v;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
//[1] :尝试使用小整数对象池
if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
v = small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
return (PyObject *) v;
}
#endif
//[2] :为通用整数对象池申请新的内存空间
if (free_list == NULL) {
if ((free_list = fill_free_list()) == NULL)
return NULL;
}
//[3] : (inline)内联PyObject_New的行为
v = free_list;
free_list = (PyIntObject *)v->ob_type;
PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
return (PyObject *) v;
}

Python根据整数对象值大小将整数对象分成大整数对象小整数对象, 两类对象都采用对象池来提高性能,但也有些不同

小整数对象创建

1
2
3
4
5
6
7
8
9
10
11
[intobject.c]
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0 // 用来判断是否定义小整数对象
// 定义一个small_ints数组用来存储所有的小整数对象的指针
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
#endif

Python为小整数对象定义了一个值区间, 默认是[-5, 257), 并定义一个small_ints数组用来存储所有的小整数对象的指针

这些小整数对象是在哪里初始化?

答案是在Python初始化的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[intobject.c]
int _PyInt_Init(void)
{
PyIntObject *v;
int ival;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
// 遍历所有的小整数
for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++)
{
// free_list 指针是用来指向可复用的内存
// 如果free_list指向的地址是null,则说明没有可用的内存空间, Python会调用fill_free_list去开辟一个内存
if (!free_list && (free_list = fill_free_list()) == NULL)
return 0;
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;
}

_PyInt_Init 所做的事就是为所有的小整数对象分配内存,并初始化

Python初始化的时候,_PyInt_Init被调用,内存被申请,小整数对象被创建,然后就仙福永享,寿与天齐了

大整数对象的创建

小整数对象因为使用的频率比大整数对象高,所有可以全部换成在内存中,从而避免创建对象导致的开销, 对于大整数, Python采用的内存复用的策略, 即Python因创建大整数所开辟的内存,并不会归还给操作系统,而是给自己复用, 当某内存上的整数对象无效的时候, 其内存会被用于新创建的整数对象.

基于上面的内存复用策略(大整数对象池), 所以需要考虑Python是怎么管理这些内存的?

  • Python需要去维护可以被复用的内存,对象创建前从可用内存中分配给新的对象使用,当对象失效时,将对象占用的内存放到可复用的内存中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#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]; // 每个_intblock块, 可以储存N_INTOBJECTS个对象
};

typedef struct _intblock PyIntBlock;

// 初始block_list, free_list 都指向null
static PyIntBlock *block_list = NULL;
static PyIntObject *free_list = NULL;

static PyIntObject *
fill_free_list(void) // 用来开辟一块(PyIntBlock)可用的内存
{
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();
// 新的block_list指向新分配的PyIntBlock, 新的block_list的next是旧的block_list
((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. */

// 这段代码做的是,将object里面的N_INTOBJECTS个PyIntObject大小的内存, 通过ob_type以链表的形式链接起来
p = &((PyIntBlock *)p)->objects[0];
q = p + N_INTOBJECTS;
// 从objects最后一个值开始,通过ob_type链接上一个值, objects中的最后一个元素的ob_type指向null
// 不知道为啥从最后开始链接成链表???
while (--q > p)
q->ob_type = (struct _typeobject *)(q-1);
q->ob_type = NULL;
return p + N_INTOBJECTS - 1;
}

如果你要去开辟一个内存用于存储对象, 最好的方式是一次性开辟一块内存,可以用于存储多个对象, Python每次开辟的内存大小是PyIntBlock大小的内存, 可以存储N_INTOBJECTS个PyIntObject对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Integers are quite normal objects, to make object handling uniform.
(Using odd pointers to represent integers would save much space
but require extra checks for this special case throughout the code.)
Since a typical Python program spends much of its time allocating
and deallocating integers, these operations should be very fast.
Therefore we use a dedicated allocation scheme with a much lower
overhead (in space and time) than straight malloc(): a simple
dedicated free list, filled when necessary with memory from malloc().

block_list is a singly-linked list of all PyIntBlocks ever allocated,
linked via their next members. PyIntBlocks are never returned to the
system before shutdown (PyInt_Fini).

free_list is a singly-linked list of available PyIntObjects, linked
via abuse of their ob_type members.
*/

这是官方的解释:

  • free_list 是一个单链表, 每个元素是一个PyIntObjects对象, 这个对象所占用的空间是可被复用
  • block_list 也是一个单链表, 其链接的是所有分配过的PyIntBlocks, 链首永远都是最新创建的PyIntBlocks, 可以通过next查看历史创建的PyIntBlocks(可以参考fill_free_list里面代码)

还有一个问题没有解决, Python是如果复用已被释放对象所占用的内存的?

这个问题可以在tp_dealloc代码里面找到答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[intobject.c]

#define PyInt_CheckExact(op) ((op)->ob_type == &PyInt_Type) // 判断是否是int类型

static void int_dealloc(PyIntObject *v)
{
if (PyInt_CheckExact(v)) {
// 将被释放的对象插入到free_list链首, 从而达到复用已释放的对象内存!!!
v->ob_type = (struct _typeobject *)free_list;
free_list = v;
}
else
// 非法对象,则直接释放
v->ob_type->tp_free((PyObject *)v);
}

本文标题:Python源码剖析第二章 整数对象

文章作者:定。

发布时间:2019年4月9日 - 00时04分

本文字数:3,987字

原始链接:http://cocofe.cn/2019/04/09/python源码剖析第二章/

许可协议: Attribution-NonCommercial 4.0

转载请保留以上信息。