Python整數對象池:「內存泄漏」?

「牆上的斑點」

我第一次注意到短褲上的那個破洞,大概是在金年的三月上旬。若是想要知道具體的時間,那就得回想一下當時我看見的東西。我還可以回憶起,游泳池頂上,搖曳的、白色的燈光不停地映在個人短褲上;有三五名少年一同扎進了水裏。哦,那是大概是冬天,由於我回憶起當時的前一天我和室友吃了部隊鍋,那段時間我沒有吸菸,反而嚼了許多口香糖,糖紙老是掉下去,無心中埋下頭,這是我第一次看到短褲上的那個破洞。html


今天在機場等shuttle時,聽到旁邊的兩個年輕人精神煥發地討論游泳的話題。莫名地回想起來,幾年前看了一篇講述Python內部整數對象管理機制的文章,其中談到了Python應用內存池機制來對「大」整數對象進行管理。從它出發,我想到了一些問題,想要在這裏討論一下。python

背景

注:本文討論的Python版本是Python 2 (2.7.11),C實現。數組

「一切皆對象」

咱們知道,Python的對象,本質上是C中的結構體(生存於在系統堆上)。全部Python對象的根源都是PyObject這個結構體。緩存

打開Python源碼目錄的Include/,能夠找到object.h這一文件,這一個文件,是整個Python對象機制的基礎。搜索PyObject,咱們將會找到:app

typedef struct _object {
    PyObject_HEAD
} PyObject;

再看看PyObject_HEAD這個宏:函數

#define PyObject_HEAD            \
    _PyObject_HEAD_EXTRA        \
    Py_ssize_t ob_refcnt;        \
    struct _typeobject *ob_type;

在實際編譯出的PyObject中,有ob_refcnt這個變量和ob_type這個指針。前者用於Python的引用計數垃圾收集,後者用於指定這個對象的「類型對象」。Python中能夠把對象分爲「普通」對象和類型對象。也就是說,表示對象的類型,是經過一個指針來指向另外一個對象,即類型對象,來實現的。這是「一切皆對象」的一個關鍵體現。性能

Python中的整數對象

Python裏面,整數對象的頭文件intobject.h,也能夠在Include/目錄裏找到,這一文件定義了PyIntObject這一結構體做爲Python中的整數對象:學習

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

上面提過了,每個Python對象的ob_type都指向一個類型對象,這裏PyIntObject則指向PyInt_Type。想要了解PyInt_Type的相關信息,咱們能夠打開intobject.c,並找到以下內容:this

PyTypeObject PyInt_Type = {
    PyObject_HEAD_INIT(&PyType_Type)
    0,
    "int",
    sizeof(PyIntObject),
    0,
    (destructor)int_dealloc,        /* tp_dealloc */
    (printfunc)int_print,            /* tp_print */
    0,                    /* tp_getattr */
    0,                    /* tp_setattr */
    (cmpfunc)int_compare,            /* tp_compare */
    (reprfunc)int_repr,            /* tp_repr */
    &int_as_number,                /* tp_as_number */
    0,                    /* tp_as_sequence */
    0,                    /* tp_as_mapping */
    (hashfunc)int_hash,            /* tp_hash */
        0,                    /* tp_call */
        (reprfunc)int_repr,            /* tp_str */
    PyObject_GenericGetAttr,        /* tp_getattro */
    0,                    /* tp_setattro */
    0,                    /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES |
        Py_TPFLAGS_BASETYPE,        /* tp_flags */
    int_doc,                /* tp_doc */
    0,                    /* tp_traverse */
    0,                    /* tp_clear */
    0,                    /* tp_richcompare */
    0,                    /* tp_weaklistoffset */
    0,                    /* tp_iter */
    0,                    /* tp_iternext */
    int_methods,                /* tp_methods */
    0,                    /* tp_members */
    0,                    /* tp_getset */
    0,                    /* tp_base */
    0,                    /* tp_dict */
    0,                    /* tp_descr_get */
    0,                    /* tp_descr_set */
    0,                    /* tp_dictoffset */
    0,                    /* tp_init */
    0,                    /* tp_alloc */
    int_new,                /* tp_new */
    (freefunc)int_free,                   /* tp_free */
};

這裏給Python的整數類型定義了許多的操做。拿int_dealloc,int_freeint_new這幾個操做舉例。顯而易見,int_dealloc負責析構,int_free負責釋放該對象所佔用的內存,int_new負責建立新的對象。int_as_number也是比較有意思的一個field。它指向一個PyNumberMethods結構體。PyNumberMethods含有許多個函數指針,用以定義對數字的操做,好比加減乘除等等。spa

通用整數對象池

Python裏面,對象的建立通常是經過Python的C API或者是其類型對象。這裏就不詳述具體的建立機制,具體內容能夠參考Python的有關文檔。這裏咱們想要關注的是,整數對象是如何存活在系統內存中的。

整數對象大概會是常見Python程序中使用最頻繁的對象了。而且,正如上面提到過的,Python的一切皆對象並且對象都生存在系統的堆上,整數對象固然不例外,那麼以整數對象的使用頻度,系統堆將面臨不可思議的高頻的訪問。一些簡單的循環和計算,都會導致malloc和free一次次被調用,由此帶來的開銷是難以計數的。此外,heap也會有不少的fragmentation的狀況,進一步致使性能降低。

這也是爲何通用整數對象池機制在Python中獲得了應用。這裏須要說明的是,「小」的整數對象,將所有直接放置於內存中。怎麼樣定義「小」呢?繼續看intobject.c,咱們能夠看到:

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* References to small integers are saved in this array so that they
   can be shared.
   The integers that are saved are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

值在這個範圍內的整數對象將被直接換存在內存中,small_ints負責保存它們的指針。能夠理解,這個數組越大,使用整數對象的性能(極可能)就越高。可是這裏也是一個trade-off,畢竟系統內存大小有限,直接緩存的小整數數量太多也會影響總體效率。

與小整數相對的是「大」整數對象,也就是除開小整數對象以外的其餘整數對象。既然不可能再緩存全部,或者說大部分經常使用範圍的整數,那麼一個妥協的辦法就是提供一片空間讓這些大整數對象按需依次使用。Python也正是這麼作的。它維護了兩個單向鏈表block_listfree_list。前者保存了許多被稱爲PyIntBlock的結構,用於存儲被使用的大整數的PyIntObject;後者則用於維護前者全部block之中的空閒內存。

仍舊是在intobject.c之中,咱們能夠看到:

struct _intblock {
    struct _intblock *next;
    PyIntObject objects[N_INTOBJECTS];
};

typedef struct _intblock PyIntBlock;

一個PyIntBlock保存N_INTOBJECTS個PyIntObject。

如今咱們來思考一下一個Python整數對象在內存中的「一輩子」。

被建立出來以前,先檢查其值的大小,若是在小整數的範圍內,則直接使用小整數池,只用更新其對應整數對象的引用計數就能夠了。若是是大整數,則須要先檢查free_list看是否有空閒的空間,要是沒有則申請新的內存空間,更新block_listfree_list,不然就使用free_list指向的下一個空閒內存位置而且更新free_list

「內存泄漏」?

So far so good. 上述的機制能夠很好減輕fragmentation的問題,同時能夠根據所跑的程序不一樣的特色來作fine tuning從而編譯出本身認爲合適的Python。可是咱們只說了Python整數對象的「來」尚未提它的「去」。當一個整數對象的引用計數變成0之後,會發生什麼事情呢?

小整數對象自是沒必要擔憂,始終都是在內存中的;大整數對象則須要調用析構操做,int_deallocintobject.c):

static void
int_dealloc(PyIntObject *v)
{
    if (PyInt_CheckExact(v)) {
        Py_TYPE(v) = (struct _typeobject *)free_list;
        free_list = v;
    }
    else
        Py_TYPE(v)->tp_free((PyObject *)v);
}

這個PyInt_CheckExact,來自於intobject.h

#define PyInt_CheckExact(op) ((op)->ob_type == &PyInt_Type)

它起到了類型檢查的做用。因此若是這個指針v指向的不是Python原生整數對象,則int_dealloc直接調用該類型的tp_free操做;不然把再也不須要的那塊內存放入free_list之中。

Py_TYPE的定義:

#ifndef Py_TYPE
    #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type)
#endif

能夠看出,free_list所維護的單向鏈表,是使用ob_type這個field來連接先後元素的。

這也就是說,當一個大整數PyIntObject的生命週期結束時,它以前的內存不會交換給系統堆,而是經過free_list繼續被該Python進程佔有。

假若一個程序使用不少的大整數呢?假若每一個大整數只被使用一次呢?是否是很像內存泄漏?

咱們來作個簡單的計算,假如你的電腦是Macbook Air,8GB Memory,若是你的PyIntObject佔用24個Byte,那麼滿打滿算,可以存下大約357913941個整數對象。

下面作個實驗。如下程序運行在Macbook Pro (mid 2015), 2.5Ghz i7, 16 GB Memory,Python 2.7.11的環境下:

l = list()
num = 178956971

for i in range(0, num):
    l.append(i)
    if len(l) % 100000 == 0:
        l[:] = []

運行這個程序,會發現它佔用了5.44GB的內存:
5.44GB

若是把整數個數減半,好比使用89478486,則會佔用2.72GB內存(正好原來一半):
2.72GB

一個PyIntObject佔用多大內存呢?
圖片描述

講道理,24 bytes x 178956971 = 4294967304 bytes,約等於2^32,也就是4GB,那麼爲何會佔用5.44GB呢?

這並不是程序其餘部分的overhead,由於,就算你的程序只含有:

for i in range(0, 178956971):
    pass

它仍舊會佔用5.44GB內存。5.44 x 2^30 / 178956971大約等於32.64,也就是均攤下來一個整數對象佔用了32.64個Byte.

這個問題能夠做爲一個簡單的思考題,這裏就不討論了。

總結

Python的整數對象管理機制並不複雜,但也有趣,剛接觸Python的時候是很好的學習材料。細糾下來會發現有不少工程上的考慮以及與之相關的現象,值得咱們深刻挖掘。

相關文章
相關標籤/搜索