421393bac9e4d6b5f66bd51a8749fbca
Python 整数对象的内存管理

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自身也是这么设计。但是随之而来的还有几个问题,

  1. 缓存哪些数字?
  2. 使用何种方式进行缓存?

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)
top Created with Sketch.