1. 程式人生 > >Python 整數物件實現原理

Python 整數物件實現原理

整數物件在Python內部用PyIntObject結構體表示:

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

PyObject_HEAD巨集中定義的兩個屬性分別是:

int ob_refcnt;        
struct _typeobject *ob_type;

這兩個屬性是所有Python物件固有的:

  • ob_refcnt:物件的引用計數,與Python的記憶體管理機制有關,它實現了基於引用計數的垃圾收集機制
  • ob_type:用於描述Python物件的型別資訊。

由此看來PyIntObject就是一個對C語言中long型別的數值的擴充套件,出於效能考慮,對於小整數,Python使用小整數物件池small_ints快取了[-5,257)之間的整數,該範圍內的整數在Python系統中是共享的。

#define NSMALLPOSINTS           257
#define NSMALLNEGINTS           5
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

pythonblock_small_int

而超過該範圍的整數即使值相同,但物件不一定是同一個,如下所示:當a與b的值都是10000,但並不是同一個物件,而值為1的時候,a和b屬於同一個物件。

>>> a = 10000
>>> b = 10000
>>> print a is b
False
>>> a = 1
>>> b = 1
>>> print a is b
True

對於超出了[-5, 257)之間的其他整數,Python同樣提供了專門的緩衝池,供這些所謂的大整數使用,避免每次使用的時候都要不斷的malloc分配記憶體帶來的效率損耗。這塊記憶體空間就是PyIntBlock

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_list,表頭始終指向最新建立的PyIntBlock物件。

PyIntBlock有兩個屬性:next,objects。next指標指向下一個PyIntBlock物件,objects是一個PyIntObject陣列(最終會轉變成單向連結串列),它是真正用於儲存被快取的PyIntObjet物件的記憶體空間。

free_list單向連結串列是所有PyIntBlock記憶體塊中空閒的記憶體。所有空閒記憶體通過一個連結串列組織起來的好處就是在Python需要新的記憶體來儲存新的PyIntObject物件時,能夠通過free_list快速獲得所需的記憶體。

python int blcik

建立一個整數物件時,如果它在小整數範圍內,就直接從小整數緩衝池中直接返回,如果不在該範圍內,就開闢一個大整數緩衝池記憶體空間:

[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;
}

fill_free_list就是建立大整數緩衝池記憶體空間的邏輯,該函式返回一個free_list連結串列,當整數物件ival建立成功後,free_list表頭就指向了v->ob_typeob_type不是所有Python物件中表示型別資訊的欄位嗎?怎麼在這裡作為一個連線指標呢?這是Python在效能與程式碼優雅之間取中庸之道,對名稱的濫用,放棄了對型別安全的堅持。把它理解成指向下一個PyIntObject的指標即可。

[intobject.c]
static PyIntObject* fill_free_list(void)
{
    PyIntObject *p, *q;
    // 申請大小為sizeof(PyIntBlock)的記憶體空間
    // block list始終指向最新建立的PyIntBlock
    p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));
    ((PyIntBlock *)p)->next = block_list;
    block_list = (PyIntBlock *)p;

    //:將PyIntBlock中的PyIntObject陣列(objects)轉變成單向連結串列
    p = &((PyIntBlock *)p)->objects[0];
    q = p + N_INTOBJECTS;
    while (--q > p)
        // ob_type指向下一個未被使用的PyIntObject。
        q->ob_type = (struct _typeobject *)(q-1);
    q->ob_type = NULL;
    return p + N_INTOBJECTS - 1;
}

不同的PyIntBlock裡面的空閒的記憶體是怎樣連線起來構成free_list的呢?這個祕密放在了整數物件垃圾回收的時候,在PyIntObject物件的tp_dealloc操作中可以看到:

[intobject.c]
static void int_dealloc(PyIntObject *v)
{
    if (PyInt_CheckExact(v)) {
        v->ob_type = (struct _typeobject *)free_list;
        free_list = v;
    }
    else
        v->ob_type->tp_free((PyObject *)v);
}

原來PyIntObject物件銷燬時,它所佔用的記憶體並不會釋放,而是繼續被Python使用,進而將free_list表頭指向了這個要被銷燬的物件上。

總結

  • Python中的int物件就是c語言中long型別數值的擴充套件
  • 小整數物件[-5, 257]在python中是共享的
  • 整數物件都是從緩衝池中獲取的。
  • 整數物件回收時,記憶體並不會歸還給系統,而是將其物件的ob_type指向free_list,供新建立的整數物件使用

原始碼參考:


關注公眾號「Python之禪」(id:vttalk)獲取最新文章 python之禪