Hack Python 整數對象

背景

寫這篇文章的緣由是目前在看《Python源碼剖析》[1],可是這本書的做者陳儒老師剖析源碼的目的好像不是太明確,因此看上去是爲了剖析源碼而剖析源碼,致使的結果是這本書裏面的分析思路不太清楚(多是個人理解問題),並且驗證想法的方式是把變量值打印出來,固然這是種很好的方式,但使用調試工具顯然更好一點。我讀這本書和看源碼的目的很簡單:爲了理解計算機的運行,理解大型軟件工程的設計。正如文章的題目爲hack python而不是源碼閱讀,hack是一個理性的分析過程,而閱讀不少時候爲所欲爲的成分多一些。但整體的過程仍是按照書中的順序來的,這本書很明確的一點就是要作什麼不要作什麼,這一點我很喜歡。可能會是一個系列,也可能只有這一篇,並不算挖坑。我更但願從多種視角來審視Python做爲一門動態語言的各類特性。做爲一個尚未學過編譯原理的人來講這個目標顯然很難完成,但正是難完成的東西,纔有完成的意義。這篇文章的源碼均來自Python-2.5.6[2],全部分析也都是基於此,編譯環境是由Koding[3]提供的,還會用到gdb[4]做爲調試工具。python

概要

這篇文章主要從源碼和運行時的角度觀察Python的整形結構。算法

數據結構

先來看一下PyIntObject的聲明[5]:數組

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

能夠看到PyIntObject被聲明爲一個結構體,包括了Python對象元信息 和一個C語言的long型整數。而Python的Python對象元信息是什麼呢?這個問題牽扯到C語言中的宏[6]和Python類型系統的本質[f],先按下不表。數據結構

封裝了C語言long型整數的PyIntObject做爲數據結構並無什麼能讓人心潮澎湃的地方,它的迷人之處在於算法[7],也就是PyIntObject的動態組織方式,但是我不可能僅從PyIntObject上管窺到它的組織方式,須要更多的信息來達成這個目的。再來看源碼:app

#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;

這段代碼對於PyIntObject的組織方式已經說得很清楚了,不用解釋。下圖形象一點:less

正如前面所說的,這個鏈表式的數據結構仍是實在太簡單,沒多少值得把玩的地方。假設我是Python的做者,我會想首先想這門語言出現的緣由,必定是不爽於現有的某些方案,因此纔要本身創造新的方案,Python被創造爲一種動態類型語言,相比於C之類的靜態語言優點在於「動態」二字。但動態不是簡單的聲明和組織幾個數據結構就完事,須要被貫穿到這門語言運行的始終。函數

運行時狀態

下面來看一下運行時狀態,根據函數名能夠確定的是fill_free_list這個函數必然會在很早的時候被調用(來準備須要的內存),咱們先不關注它究竟是怎麼作內存分配的,先下個斷點,看一下誰第一個調用它,看到第一個觸發斷點的地方是_PyInt_Init,也就是Python整型對象(類型對象)的初始化函數,推測應該是Python中的每個類型對象都會有一個初始化函數,在Python開始運行時完成初始化工做。來看這個_PyInt_Init函數具體包含了什麼內容:工具

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 *)v->ob_type;
        PyObject_INIT(v, &PyInt_Type);
        v->ob_ival = ival;
        small_ints[ival + NSMALLNEGINTS] = v;
    }
#endif
    return 1;
}

首先,正常狀況下(排除內存不夠),free* 相似命名的函數的返回值不會是NULL,因此直接忽略掉for循環中的if,在其下設一個斷點觀察free_list此時的值(被賦值以前或直接觀察v的值),由於這是全局變量被賦值,記錄一下它以前的值,說不定之後有用。

再往下看,除了PyObject_INIT函數(咱們先無論它,等HACK Python類型系統[f]的時候再研究),還有small_ints這個奇葩數組,根據名字,這是個在Python整型對象中必然會用到的東西,因此逃不掉了,不過還好,不就是個數組嘛!ui

數據結構:small_ints

咱們往上找這個small_ints數組的聲明,看看他究竟暗藏了什麼玄機。spa

static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

發現了這一句,實在是太簡單了,一個PyIntObject指針數組。大概長這個樣子:

同時還發現了剛纔不知道的宏,早就猜中的東西,如今是多少也可有可無了。但是這個small_ints究竟是用來幹嗎的還不清楚,僅僅知道它是什麼永遠很差玩兒,爲何纔是真正須要關注的。但是,怎麼求出這個問題的答案呢?問源碼做者最直接了,但是時效性太差,放棄;上網搜,太沒挑戰,放棄;還有源碼,不知道可不能夠,要回答的問題是爲何,好比我爲何須要一臺電腦呢?回答是由於我在跑程序的時候要用。如今再來看一下_PyInt_Init對數組small_ints作了什麼。

過程:_PyInt_Init

能夠看到的是small_ints徹底是一個靜態的結構,它是在_PyInt_Init被調用也就是系統初始化時就被直接分配了_intblock塊,固然按照_intblock塊的大小,N_INTOBJECTS爲*((BLOCK_SIZE - BHEAD_SIZE) / sizeof(PyIntObject)),這是多少呢?還須要知道sizeof(PyIntObject) ,用gdb看看到這樣:

因此一個_intblock能夠容納41個PyIntObject,比small_ints的size還小(因此下面的圖有問題,不過這個信息不怎麼重要,由於能夠改small_ints的相關宏的值,讓圖變得正確)。反正在_PyInt_Init中,只要空間不夠(free_list == NULL,if條件&&左值),就調用fill_free_list分配_intblock。按照默認的參數,大概得分配7個_intblock來完成_PyInt_Init(一樣,由於要依靠參數,不重要)。

那如今,初始化過程已經完成了,咱們總結一下,_PyInt_Init的主要做用就是構建一個small_ints及其空間(在《Python源碼剖析》用小整數池來描述,我以爲這麼多概念容易confuse,因此直接把本質說一下就好),但裏面並無足夠的信息來判斷small_ints及其空間是如何被利用的,問題(爲何須要small_ints?)依然沒有被解決。_PyInt_Init這條線索雖然斷了,但好在還有PyInt_FromLong。

過程:PyInt_FromLong

注意到Python在這個時候已經經歷了各類複雜的初始化過程,打印出了它的版本信息,萬事俱備,只欠輸入。不關注輸入過程或者調用信息,假設如今就調用了PyInt_FromLong。

PyObject *
PyInt_FromLong(long ival)
{
    register PyIntObject *v;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
        v = small_ints[ival + NSMALLNEGINTS];
        Py_INCREF(v);
#ifdef COUNT_ALLOCS
        if (ival >= 0)
            quick_int_allocs++;
        else
            quick_neg_int_allocs++;
#endif
        return (PyObject *) v;
    }
#endif
    if (free_list == NULL) {
        if ((free_list = fill_free_list()) == NULL)
            return NULL;
    }
    /* Inline PyObject_New */
    v = free_list;
    free_list = (PyIntObject *)v->ob_type;
    PyObject_INIT(v, &PyInt_Type);
    v->ob_ival = ival;
    return (PyObject *) v;
}

構造一個Python整數對象須要一個long型整數,若是這個long型整數大小是在-NSMALLNEGINTS到NSMALLPOSINTS之間,就認爲它是一個小整數,在small_ints空間中找到封裝該小整數的PyIntObject並調用Py_INCREF方法。這裏經過命名能夠知道Py_INCREF方法的做用是對對象的引用數作自增操做,具體實現不深刻。

固然上面只是針對小整數的狀況,大整數是怎樣處理的呢?繼續往下看就能夠知道。過程跟_PyInt_Init中同樣,同樣的經過判斷條件語句的右值來調用fill_free_list方法。

其實大整數對象和小整數對象的區別就在於:
1. 小整數對象是在系統初始化的時候就爲其分配了內存空間PyIntBlock(也就是 _intblock),並寫入值,而對於大整數若是現有的以前分配好的PyIntBlock中有空間沒用完的話就直接把值寫入該塊(固然寫以前還要移動free_list並對對象作初始化操做),若是用完了就調用fill_free_list新建PyIntBlock。
2. 當要用一個小整數來構造小整數對象時,只對其相應的引用計數器作自增操做,而不像大整數那樣作複雜的函數調用和內存分配操做,目的固然是時間效率,典型的那空間換時間的作法。
3. 本質上兩者在內存中沒有任何區別,小整數和大整數的界限能夠看成參數來本身配置也能夠說明這一點,不過這個界限究竟設爲多少Python的效率能達到作好的平衡呢?不知道默認的參數設置成那樣的緣由是什麼,有沒有更加科學的參數?

後記

做爲第一篇關於Hack Python的文章,裏面有不少東西都比較囉嗦。要作的是還原整個探索的過程,包括全部走過的彎路,尤爲要關注的是爲何,而不只僅着眼因而什麼。

對於Python類型系統的探索須要明確如下幾點:

  1. 概念:對於概念基本原則是越少定義越好,由於不少東西本質上都是一回事,可是一些基本的約定仍是很重要的,能夠避免每次都重複囉嗦。在類型系統部分中定義以下:類型表示PyXXXObject對象,如PyIntObject;對象表示例化後的類型;類型初始化函數表示在Python初始化時調用的用來初始化類型的函數,如PyInt_Init;構造函數表示構造對象須要的函數,如PyInt_FromLong。
  2. 研究範圍:在之後的hack對象系統中,默認只研究關於本類型的內容,對於整個類型系統的宏觀概覽不涉及;除非用於比較,其餘類型不涉及;與C語言相關的基本概念不涉及,只給出資料;與研究工具相關的步驟不涉及,只給出結果和基本參考資料。主要目的在於着眼於每種類型,在研究完全部類型後再總結整個類型系統。
  3. 對於類型系統的研究由本文能夠得出如下順序:類型基本的數據結構-基本類型數據結構的組織-類型特殊過程分析和解讀-細節-總結。

文章裏面包含連接有礙於流暢閱讀,因此取消文章內的連接,在末尾加參考資料部分以示引用或概念解釋。


資料:

  • [1]《python源碼剖析》
  • [2]Python-2.5.6
  • [3]Koding
  • [4]GDB
  • [5]聲明
  • [6]宏
  • [7]Python的類型系統總結

延伸:

【轉載請註明出處 dukeyunz.com

相關文章
相關標籤/搜索