摘要:在類型系統部分中定義如下類型表示對象,如對象表示例化后的類型類型初始化函數表示在初始化時調用的用來初始化類型的函數,如構造函數表示構造對象需要的函數,如。
背景
寫這篇文章的原因是目前在看《Python源碼剖析》[1],但是這本書的作者陳儒老師剖析源碼的目的好像不是太明確,所以看上去是為了剖析源碼而剖析源碼,導致的結果是這本書里面的分析思路不太清楚(可能是我的理解問題),而且驗證想法的方式是把變量值打印出來,當然這是種很好的方式,但使用調試工具顯然更好一點。我讀這本書和看源碼的目的很簡單:為了理解計算機的運行,理解大型軟件工程的設計。正如文章的題目為hack python而不是源碼閱讀,hack是一個理性的分析過程,而閱讀很多時候隨心所欲的成分多一些。但總體的過程還是按照書中的順序來的,這本書很明確的一點就是要做什么不要做什么,這一點我很喜歡。可能會是一個系列,也可能只有這一篇,并不算挖坑。我更希望從多種視角來審視Python作為一門動態語言的各種特性。作為一個還沒有學過編譯原理的人來說這個目標顯然很難完成,但正是難完成的東西,才有完成的意義。這篇文章的源碼均來自Python-2.5.6[2],所有分析也都是基于此,編譯環境是由Koding[3]提供的,還會用到gdb[4]作為調試工具。
概要這篇文章主要從源碼和運行時的角度觀察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上管窺到它的組織方式,需要更多的信息來達成這個目的。再來看源碼:
#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的組織方式已經說得很清楚了,不用解釋。下圖形象一點:
正如前面所說的,這個鏈表式的數據結構還是實在太簡單,沒多少值得把玩的地方。假設我是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整型對象中必然會用到的東西,所以逃不掉了,不過還好,不就是個數組嘛!
我們往上找這個small_ints數組的聲明,看看他究竟暗藏了什么玄機。
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類型系統的探索需要明確以下幾點:
概念:對于概念基本原則是越少定義越好,因為很多東西本質上都是一回事,但是一些基本的約定還是很重要的,可以避免每次都重復啰嗦。在類型系統部分中定義如下:類型表示PyXXXObject對象,如PyIntObject;對象表示例化后的類型;類型初始化函數表示在Python初始化時調用的用來初始化類型的函數,如PyInt_Init;構造函數表示構造對象需要的函數,如PyInt_FromLong。
研究范圍:在以后的hack對象系統中,默認只研究關于本類型的內容,對于整個類型系統的宏觀概覽不涉及;除非用于比較,其他類型不涉及;與C語言相關的基本概念不涉及,只給出資料;與研究工具相關的步驟不涉及,只給出結果和基本參考資料。主要目的在于著眼于每種類型,在研究完所有類型后再總結整個類型系統。
對于類型系統的研究由本文可以得出以下順序:類型基本的數據結構-基本類型數據結構的組織-類型特殊過程分析和解讀-細節-總結。
文章里面包含鏈接有礙于流暢閱讀,所以取消文章內的鏈接,在末尾加參考資料部分以示引用或概念解釋。
資料:
[1]《python源碼剖析》
[2]Python-2.5.6
[3]Koding
[4]GDB
[5]聲明
[6]宏
[7]Python的類型系統總結
延伸:
使用gdb調試運行時的程序小技巧
【轉載請注明出處 dukeyunz.com】
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/45383.html
摘要:在各大瀏覽器廠商的發展過程中,它們對的標準各有不同的實現,標準不同存在差異所以產生兼容性的問題。它是一種對特定的瀏覽器或瀏覽器組顯示或隱藏規則或聲明的方法。但是及更低版本瀏覽器會繼續解析。 為什么會存在瀏覽器兼容問題? 首先要了解兼容,我們先得了解一下為什么會存在瀏覽器兼容問題。在各大瀏覽器廠商的發展過程中,它們對web的標準各有不同的實現,標準不同存在差異所以產生兼容性的問題。 瀏覽...
摘要:對象是一個值超出有效范圍時發生的錯誤。包括返回原數組包括數組對象函數可以用來判斷變量是否為對象數組對象函數構造函數與直接賦值是等價的。只適用于,數組不適用通過可以看出一個值到底是什么類型,其中返回值的第二個值表示該值的構造函數。 這是ES5的入門篇教程的筆記,網址:JavaScript教程,以下內容中黑體表示大標題,還有一些重點;斜體表示對于自身,還需要下功夫學習的內容。這里面有一些自...
摘要:另外自己寫代碼測試了下和的速度,比較結果如下位操作轉換整數的原理參考上面對于位操作的說明,點擊下面鏈接有這樣一段話中,數字存儲是雙進度位浮點數。但是位操作卻會把要操作的運算元當做位帶符號的整數。因此進行位操作時,會自動把數字先轉換為整數。 本文將會列舉并說明JavaScript 把一個number(或者numerical的對象)轉換成一個整數相關方法。 使用parseInt parse...
摘要:這里需要說明的是,小的整數對象,將全部直接放置于內存中。內存泄漏上述的機制可以很好減輕的問題,同時可以根據所跑的程序不同的特點來做從而編譯出自己認為合適的。 墻上的斑點 我第一次注意到短褲上的那個破洞,大概是在金年的三月上旬。如果想要知道具體的時間,那就得回想一下當時我看見的東西。我還能夠回憶起,游泳池頂上,搖曳的、白色的燈光不停地映在我的短褲上;有三五名少年一同扎進了水里。哦,那是大...
閱讀 2178·2021-11-24 09:38
閱讀 3242·2021-11-08 13:27
閱讀 3083·2021-09-10 10:51
閱讀 3143·2019-08-29 12:20
閱讀 663·2019-08-28 18:28
閱讀 3459·2019-08-26 11:53
閱讀 2706·2019-08-26 11:46
閱讀 1515·2019-08-26 10:56