之前分析过浮点数的创建,而整数的创建与之是类似的,都是基于字面量形式的 C 数据创建 Python 数据。还是那句话,Python 底层的 API 设计的很优美,都非常的相似,比如:
创建浮点数可以使用 PyFloat_FromDouble、PyFloat_FromString 等等;
创建整数可以使用 PyLong_FromLong、PyLong_FromDouble、PyLong_FromString 等等。
创建整数的这些函数直接去 Objects/longobject.c 里面查看即可,这里就不多说了,我们重点来看一下小整数对象池。
我们知道整数属于不可变对象,运算之后会创建新的对象。
>>> a = 666
>>> id(a)
140018977594800
>>> a += 1
>>> id(a)
140018977588112
>>>
显然这种做法一定存在性能缺陷,因为程序运行时会有大量的对象创建和销毁。根据浮点数的经验,我们猜测 Python 应该也对整数使用了缓存池吧。答案是差不多,只不过不是缓存池,而是小整数对象池。
Python 将那些使用频率高的整数预先创建好,而且都是单例模式,这些预先创建好的整数会放在一个静态数组里面,我们称为小整数对象池。如果需要使用的话会直接拿来用,而不用重新创建。注意:这些整数在解释器启动的时候,就已经创建了。
看一下小整数对象池的实现。
// Include/internal/pycore_global_objects.h
#define _PY_NSMALLPOSINTS 257
#define _PY_NSMALLNEGINTS 5
struct _Py_static_objects {
// 该结构体保存了一些静态分配的对象,比如这里的小整数对象池
struct {
PyLongObject small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS];
// ...
} singletons;
};
结构体的 small_ints 字段便是小整数对象池,它是一个类型为 PyLongObject、长度为 262 的数组,里面保存了 -5 到 256 之间的整数。
然后再来看一下它的初始化过程。
// Include/internal/pycore_runtime_init.h
#define _PyRuntimeState_INIT(runtime) \
{ \
/* ... */ \
.static_objects = { \
.singletons = { \
.small_ints = _Py_small_ints_INIT, \
}, \
}, \
}
// runtime 保存了程序运行时的环境和行为,比如内存分配、异常处理等等
// 上面的代码我们只截取了一小部分,可以看到小整数对象池被初始化为 _Py_small_ints_INIT
// Include/internal/pycore_runtime_init_generated.h
#define _Py_small_ints_INIT { \
_PyLong_DIGIT_INIT(-5), \
_PyLong_DIGIT_INIT(-4), \
_PyLong_DIGIT_INIT(-3), \
_PyLong_DIGIT_INIT(-2), \
_PyLong_DIGIT_INIT(-1), \
_PyLong_DIGIT_INIT(0), \
_PyLong_DIGIT_INIT(1), \
_PyLong_DIGIT_INIT(2), \
/* ... */ \
_PyLong_DIGIT_INIT(252), \
_PyLong_DIGIT_INIT(253), \
_PyLong_DIGIT_INIT(254), \
_PyLong_DIGIT_INIT(255), \
_PyLong_DIGIT_INIT(256), \
}
所以解释器在启动的时候就会预先创建一个可以容纳 262 个整数的数组,并会依次初始化值为 -5 到 256 之间的 PyLongObject。当然这只是解释器的默认行为,因为 -5 到 256 之间的整数使用频率最高,但你也可以根据自身情况修改源码,让它缓存更多的整数,以提升效率,当然这也会额外占用一些内存。