寫這篇文章的緣由是目前在看《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數組的聲明,看看他究竟暗藏了什麼玄機。spa
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
發現了這一句,實在是太簡單了,一個PyIntObject指針數組。大概長這個樣子:
同時還發現了剛纔不知道的宏,早就猜中的東西,如今是多少也可有可無了。但是這個small_ints究竟是用來幹嗎的還不清楚,僅僅知道它是什麼永遠很差玩兒,爲何纔是真正須要關注的。但是,怎麼求出這個問題的答案呢?問源碼做者最直接了,但是時效性太差,放棄;上網搜,太沒挑戰,放棄;還有源碼,不知道可不能夠,要回答的問題是爲何,好比我爲何須要一臺電腦呢?回答是由於我在跑程序的時候要用。如今再來看一下_PyInt_Init對數組small_ints作了什麼。
能夠看到的是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。
注意到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類型系統的探索須要明確如下幾點:
文章裏面包含連接有礙於流暢閱讀,因此取消文章內的連接,在末尾加參考資料部分以示引用或概念解釋。
資料:
延伸:
【轉載請註明出處 dukeyunz.com】