Python整数的内存管理
上篇文章我们阐述了Python对整数对象的设计后,我们会发现,大量操作后会产生大量的PyIntObject,占据大量的堆空间。更有甚者,当我们试图在诸如for循环之类的操作中快速大量创建临时性整数时,会频繁申请堆内存从而引发大量的内存碎片,可能对整体性能产生出来严重的影响。
因此,这篇文章我们一起来探究下Python究竟是如何优化这些负面影响的。
整数的区分
对密码学有研究的人都知道,英文字母总共有26个,但是他们在日常生活(比如写作)中出现的频率却各不同,比如e可能是字母中出现频次最高的一个。
而在我们日常编程过程中,虽然整数的使用无处不在,但是却有一部分数字明显会比其余的频次高出许多,大致举几个例子:
for i in range(0, 10)
a + b > 0
y = 7a + 8b + 3c + 2z
a + b > INT_MAX
不难看出,基本的逻辑循环、分支控制以及运算、越界判断等等,常常会和部分数字打交道。上文《Python的整数设计》中我们知道,整数的每次运算等操作始终都会创建一个新的对象,那么如果我们这些经常遇到的数字都需要多次创建分配,那势必造成大量的内存调用,不仅仅影响性能,还会产生大量的内存碎片。
那么,有什么比较好的解决思路吗?
可能有些朋友已经会想到,既然这些数字经常用到,那我们建立一个缓存池不就好了吗?
没错!Python自身也是这么设计。但是随之而来的还有几个问题,
缓存哪些数字?
使用何种方式进行缓存?
Python的设计是将数字区分为小整数和大整数对象。对于小整数对象,采用数组直接全部缓存,而对于大整数对象,采用链表的方式循环复用特定个数的内存池。
小整数缓存池
首先我们先来探究下全部缓存的小整数对象,想必各位可以一样都有困惑,究竟哪些整数可以归为小整数呢?
ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
// 缓存池
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
#endif
从[intobject.c]中我们可以发现,Python源代码将-5(包含)到257(不包含)这段连续区间的数字定义为小整数,利用指针数组全部进行了缓存,而数组的index直接可以快速索引,也避免了单独构建映射表的内存开销。
还有一点需要注意,小整数的缓存并不是通常所认为的懒加载的构造方式。而是Python在运行时初始化的时候,一次性直接初始化构建完:
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);
(void)PyObject_INIT(v, &PyInt_Type);
v->ob_ival = ival;
small_ints[ival + NSMALLNEGINTS] = v;
}
#endif
return 1;
}
大整数缓存池
谈完了小整数的缓存池设计,我们再来谈谈那些大整数的设计。Python对大整数并未采取对象复用的策略,但是在减少内存申请和碎片化优化上还是做了不少设计。Python自建了一个内存池的概念,当通用对象不在使用(引用计数为0),所占用的内存继续利用;除此之外,当内存池容量不够的时候,是以固定个数向系统申请一批内存。
// 结构定义
#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;
// 申请内存
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)