Hack Python 整數物件

發表於2016-06-13

背景

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

概要

這篇文章主要從原始碼和執行時的角度觀察Python的整形結構。

資料結構

先來看一下PyIntObject的宣告[5]:

可以看到PyIntObject被宣告為一個結構體,包括了Python物件元資訊 和一個C語言的long型整數。而Python的Python物件元資訊是什麼呢?這個問題牽扯到C語言中的巨集[6]和Python型別系統的本質[f],先按下不表。

封裝了C語言long型整數的PyIntObject作為資料結構並沒有什麼能讓人心潮澎湃的地方,它的迷人之處在於演算法[7],也就是PyIntObject的動態組織方式,可是我不可能僅從PyIntObject上管窺到它的組織方式,需要更多的資訊來達成這個目的。再來看原始碼:

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

Hack Python 整數物件

正如前面所說的,這個連結串列式的資料結構還是實在太簡單,沒多少值得把玩的地方。假設我是Python的作者,我會想首先想這門語言出現的原因,一定是不爽於現有的某些方案,所以才要自己創造新的方案,Python被創造為一種動態型別語言,相比於C之類的靜態語言優勢在於“動態”二字。但動態不是簡單的宣告和組織幾個資料結構就完事,需要被貫穿到這門語言執行的始終。

執行時狀態

下面來看一下執行時狀態,根據函式名可以肯定的是fill_free_list這個函式必然會在很早的時候被呼叫(來準備需要的記憶體),我們先不關注它到底是怎麼做記憶體分配的,先下個斷點,看一下誰第一個呼叫它,看到第一個觸發斷點的地方是_PyInt_Init,也就是Python整型物件(型別物件)的初始化函式,推測應該是Python中的每一個型別物件都會有一個初始化函式,在Python開始執行時完成初始化工作。來看這個_PyInt_Init函式具體包含了什麼內容:

首先,正常情況下(排除記憶體不夠),free* 類似命名的函式的返回值不會是NULL,所以直接忽略掉for迴圈中的if,在其下設一個斷點觀察free_list此時的值(被賦值之前或直接觀察v的值),因為這是全域性變數被賦值,記錄一下它之前的值,說不定以後有用。
Hack Python 整數物件
再往下看,除了PyObject_INIT函式(我們先不管它,等HACK Python型別系統[f]的時候再研究),還有small_ints這個奇葩陣列,根據名字,這是個在Python整型物件中必然會用到的東西,所以逃不掉了,不過還好,不就是個陣列嘛!

資料結構:small_ints

我們往上找這個small_ints陣列的宣告,看看他究竟暗藏了什麼玄機。

發現了這一句,實在是太簡單了,一個PyIntObject指標陣列。大概長這個樣子:
Hack Python 整數物件
同時還發現了剛才不知道的巨集,早就猜中的東西,現在是多少也無關緊要了。可是這個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看看到這樣:
Hack Python 整數物件
所以一個_intblock可以容納41個PyIntObject,比small_ints的size還小(所以下面的圖有問題,不過這個資訊不怎麼重要,因為可以改small_ints的相關巨集的值,讓圖變得正確)。反正在_PyInt_Init中,只要空間不夠(free_list == NULL,if條件&&左值),就呼叫fill_free_list分配_intblock。按照預設的引數,大概得分配7個_intblock來完成_PyInt_Init(同樣,因為要依靠引數,不重要)。
Hack Python 整數物件
那現在,初始化過程已經完成了,我們總結一下,_PyInt_Init的主要作用就是構建一個small_ints及其空間(在《Python原始碼剖析》用小整數池來描述,我覺得這麼多概念容易confuse,所以直接把本質說一下就好),但裡面並沒有足夠的資訊來判斷small_ints及其空間是如何被利用的,問題(為什麼需要small_ints?)依然沒有被解決。_PyInt_Init這條線索雖然斷了,但好在還有PyInt_FromLong。

過程:PyInt_FromLong

注意到Python在這個時候已經經歷了各種複雜的初始化過程,列印出了它的版本資訊,萬事俱備,只欠輸入。不關注輸入過程或者呼叫資訊,假設現在就呼叫了PyInt_FromLong。

構造一個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的型別系統總結

延伸:

相關文章