摘要:在類型系統(tǒng)部分中定義如下類型表示對象,如對象表示例化后的類型類型初始化函數(shù)表示在初始化時調(diào)用的用來初始化類型的函數(shù),如構(gòu)造函數(shù)表示構(gòu)造對象需要的函數(shù),如。
背景
寫這篇文章的原因是目前在看《Python源碼剖析》[1],但是這本書的作者陳儒老師剖析源碼的目的好像不是太明確,所以看上去是為了剖析源碼而剖析源碼,導(dǎo)致的結(jié)果是這本書里面的分析思路不太清楚(可能是我的理解問題),而且驗證想法的方式是把變量值打印出來,當(dāng)然這是種很好的方式,但使用調(diào)試工具顯然更好一點(diǎn)。我讀這本書和看源碼的目的很簡單:為了理解計算機(jī)的運(yùn)行,理解大型軟件工程的設(shè)計。正如文章的題目為hack python而不是源碼閱讀,hack是一個理性的分析過程,而閱讀很多時候隨心所欲的成分多一些。但總體的過程還是按照書中的順序來的,這本書很明確的一點(diǎn)就是要做什么不要做什么,這一點(diǎn)我很喜歡??赡軙且粋€系列,也可能只有這一篇,并不算挖坑。我更希望從多種視角來審視Python作為一門動態(tài)語言的各種特性。作為一個還沒有學(xué)過編譯原理的人來說這個目標(biāo)顯然很難完成,但正是難完成的東西,才有完成的意義。這篇文章的源碼均來自Python-2.5.6[2],所有分析也都是基于此,編譯環(huán)境是由Koding[3]提供的,還會用到gdb[4]作為調(diào)試工具。
概要這篇文章主要從源碼和運(yùn)行時的角度觀察Python的整形結(jié)構(gòu)。
數(shù)據(jù)結(jié)構(gòu)先來看一下PyIntObject的聲明[5]:
typedef struct { PyObject_HEAD long ob_ival; } PyIntObject;
可以看到PyIntObject被聲明為一個結(jié)構(gòu)體,包括了Python對象元信息 和一個C語言的long型整數(shù)。而Python的Python對象元信息是什么呢?這個問題牽扯到C語言中的宏[6]和Python類型系統(tǒng)的本質(zhì)[f],先按下不表。
封裝了C語言long型整數(shù)的PyIntObject作為數(shù)據(jù)結(jié)構(gòu)并沒有什么能讓人心潮澎湃的地方,它的迷人之處在于算法[7],也就是PyIntObject的動態(tài)組織方式,可是我不可能僅從PyIntObject上管窺到它的組織方式,需要更多的信息來達(dá)成這個目的。再來看源碼:
#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的組織方式已經(jīng)說得很清楚了,不用解釋。下圖形象一點(diǎn):
正如前面所說的,這個鏈表式的數(shù)據(jù)結(jié)構(gòu)還是實在太簡單,沒多少值得把玩的地方。假設(shè)我是Python的作者,我會想首先想這門語言出現(xiàn)的原因,一定是不爽于現(xiàn)有的某些方案,所以才要自己創(chuàng)造新的方案,Python被創(chuàng)造為一種動態(tài)類型語言,相比于C之類的靜態(tài)語言優(yōu)勢在于“動態(tài)”二字。但動態(tài)不是簡單的聲明和組織幾個數(shù)據(jù)結(jié)構(gòu)就完事,需要被貫穿到這門語言運(yùn)行的始終。
運(yùn)行時狀態(tài)下面來看一下運(yùn)行時狀態(tài),根據(jù)函數(shù)名可以肯定的是fill_free_list這個函數(shù)必然會在很早的時候被調(diào)用(來準(zhǔn)備需要的內(nèi)存),我們先不關(guān)注它到底是怎么做內(nèi)存分配的,先下個斷點(diǎn),看一下誰第一個調(diào)用它,看到第一個觸發(fā)斷點(diǎn)的地方是_PyInt_Init,也就是Python整型對象(類型對象)的初始化函數(shù),推測應(yīng)該是Python中的每一個類型對象都會有一個初始化函數(shù),在Python開始運(yùn)行時完成初始化工作。來看這個_PyInt_Init函數(shù)具體包含了什么內(nèi)容:
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; }
首先,正常情況下(排除內(nèi)存不夠),free* 類似命名的函數(shù)的返回值不會是NULL,所以直接忽略掉for循環(huán)中的if,在其下設(shè)一個斷點(diǎn)觀察free_list此時的值(被賦值之前或直接觀察v的值),因為這是全局變量被賦值,記錄一下它之前的值,說不定以后有用。
再往下看,除了PyObject_INIT函數(shù)(我們先不管它,等HACK Python類型系統(tǒng)[f]的時候再研究),還有small_ints這個奇葩數(shù)組,根據(jù)名字,這是個在Python整型對象中必然會用到的東西,所以逃不掉了,不過還好,不就是個數(shù)組嘛!
我們往上找這個small_ints數(shù)組的聲明,看看他究竟暗藏了什么玄機(jī)。
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
發(fā)現(xiàn)了這一句,實在是太簡單了,一個PyIntObject指針數(shù)組。大概長這個樣子:
同時還發(fā)現(xiàn)了剛才不知道的宏,早就猜中的東西,現(xiàn)在是多少也無關(guān)緊要了??墒沁@個small_ints到底是用來干嘛的還不清楚,僅僅知道它是什么永遠(yuǎn)不好玩兒,為什么才是真正需要關(guān)注的。可是,怎么求出這個問題的答案呢?問源碼作者最直接了,可是時效性太差,放棄;上網(wǎng)搜,太沒挑戰(zhàn),放棄;還有源碼,不知道可不可以,要回答的問題是為什么,比如我為什么需要一臺電腦呢?回答是因為我在跑程序的時候要用?,F(xiàn)在再來看一下_PyInt_Init對數(shù)組small_ints做了什么。
可以看到的是small_ints完全是一個靜態(tài)的結(jié)構(gòu),它是在_PyInt_Init被調(diào)用也就是系統(tǒng)初始化時就被直接分配了_intblock塊,當(dāng)然按照_intblock塊的大小,N_INTOBJECTS為*((BLOCK_SIZE - BHEAD_SIZE) / sizeof(PyIntObject)),這是多少呢?還需要知道sizeof(PyIntObject) ,用gdb看看到這樣:
所以一個_intblock可以容納41個PyIntObject,比small_ints的size還小(所以下面的圖有問題,不過這個信息不怎么重要,因為可以改small_ints的相關(guān)宏的值,讓圖變得正確)。反正在_PyInt_Init中,只要空間不夠(free_list == NULL,if條件&&左值),就調(diào)用fill_free_list分配_intblock。按照默認(rèn)的參數(shù),大概得分配7個_intblock來完成_PyInt_Init(同樣,因為要依靠參數(shù),不重要)。
那現(xiàn)在,初始化過程已經(jīng)完成了,我們總結(jié)一下,_PyInt_Init的主要作用就是構(gòu)建一個small_ints及其空間(在《Python源碼剖析》用小整數(shù)池來描述,我覺得這么多概念容易confuse,所以直接把本質(zhì)說一下就好),但里面并沒有足夠的信息來判斷small_ints及其空間是如何被利用的,問題(為什么需要small_ints?)依然沒有被解決。_PyInt_Init這條線索雖然斷了,但好在還有PyInt_FromLong。
注意到Python在這個時候已經(jīng)經(jīng)歷了各種復(fù)雜的初始化過程,打印出了它的版本信息,萬事俱備,只欠輸入。不關(guān)注輸入過程或者調(diào)用信息,假設(shè)現(xiàn)在就調(diào)用了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; }
構(gòu)造一個Python整數(shù)對象需要一個long型整數(shù),如果這個long型整數(shù)大小是在-NSMALLNEGINTS到NSMALLPOSINTS之間,就認(rèn)為它是一個小整數(shù),在small_ints空間中找到封裝該小整數(shù)的PyIntObject并調(diào)用Py_INCREF方法。這里通過命名可以知道Py_INCREF方法的作用是對對象的引用數(shù)做自增操作,具體實現(xiàn)不深入。
當(dāng)然上面只是針對小整數(shù)的情況,大整數(shù)是怎樣處理的呢?繼續(xù)往下看就可以知道。過程跟_PyInt_Init中一樣,一樣的通過判斷條件語句的右值來調(diào)用fill_free_list方法。
其實大整數(shù)對象和小整數(shù)對象的區(qū)別就在于:
1. 小整數(shù)對象是在系統(tǒng)初始化的時候就為其分配了內(nèi)存空間PyIntBlock(也就是 _intblock),并寫入值,而對于大整數(shù)如果現(xiàn)有的之前分配好的PyIntBlock中有空間沒用完的話就直接把值寫入該塊(當(dāng)然寫之前還要移動free_list并對對象做初始化操作),如果用完了就調(diào)用fill_free_list新建PyIntBlock。
2. 當(dāng)要用一個小整數(shù)來構(gòu)造小整數(shù)對象時,只對其相應(yīng)的引用計數(shù)器做自增操作,而不像大整數(shù)那樣做復(fù)雜的函數(shù)調(diào)用和內(nèi)存分配操作,目的當(dāng)然是時間效率,典型的那空間換時間的做法。
3. 本質(zhì)上二者在內(nèi)存中沒有任何區(qū)別,小整數(shù)和大整數(shù)的界限可以當(dāng)作參數(shù)來自己配置也可以說明這一點(diǎn),不過這個界限究竟設(shè)為多少Python的效率能達(dá)到做好的平衡呢?不知道默認(rèn)的參數(shù)設(shè)置成那樣的原因是什么,有沒有更加科學(xué)的參數(shù)?
作為第一篇關(guān)于Hack Python的文章,里面有很多東西都比較啰嗦。要做的是還原整個探索的過程,包括所有走過的彎路,尤其要關(guān)注的是為什么,而不僅僅著眼于是什么。
對于Python類型系統(tǒng)的探索需要明確以下幾點(diǎn):
概念:對于概念基本原則是越少定義越好,因為很多東西本質(zhì)上都是一回事,但是一些基本的約定還是很重要的,可以避免每次都重復(fù)啰嗦。在類型系統(tǒng)部分中定義如下:類型表示PyXXXObject對象,如PyIntObject;對象表示例化后的類型;類型初始化函數(shù)表示在Python初始化時調(diào)用的用來初始化類型的函數(shù),如PyInt_Init;構(gòu)造函數(shù)表示構(gòu)造對象需要的函數(shù),如PyInt_FromLong。
研究范圍:在以后的hack對象系統(tǒng)中,默認(rèn)只研究關(guān)于本類型的內(nèi)容,對于整個類型系統(tǒng)的宏觀概覽不涉及;除非用于比較,其他類型不涉及;與C語言相關(guān)的基本概念不涉及,只給出資料;與研究工具相關(guān)的步驟不涉及,只給出結(jié)果和基本參考資料。主要目的在于著眼于每種類型,在研究完所有類型后再總結(jié)整個類型系統(tǒng)。
對于類型系統(tǒng)的研究由本文可以得出以下順序:類型基本的數(shù)據(jù)結(jié)構(gòu)-基本類型數(shù)據(jù)結(jié)構(gòu)的組織-類型特殊過程分析和解讀-細(xì)節(jié)-總結(jié)。
文章里面包含鏈接有礙于流暢閱讀,所以取消文章內(nèi)的鏈接,在末尾加參考資料部分以示引用或概念解釋。
資料:
[1]《python源碼剖析》
[2]Python-2.5.6
[3]Koding
[4]GDB
[5]聲明
[6]宏
[7]Python的類型系統(tǒng)總結(jié)
延伸:
使用gdb調(diào)試運(yùn)行時的程序小技巧
【轉(zhuǎn)載請注明出處 dukeyunz.com】
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/45383.html
摘要:在各大瀏覽器廠商的發(fā)展過程中,它們對的標(biāo)準(zhǔn)各有不同的實現(xiàn),標(biāo)準(zhǔn)不同存在差異所以產(chǎn)生兼容性的問題。它是一種對特定的瀏覽器或瀏覽器組顯示或隱藏規(guī)則或聲明的方法。但是及更低版本瀏覽器會繼續(xù)解析。 為什么會存在瀏覽器兼容問題? 首先要了解兼容,我們先得了解一下為什么會存在瀏覽器兼容問題。在各大瀏覽器廠商的發(fā)展過程中,它們對web的標(biāo)準(zhǔn)各有不同的實現(xiàn),標(biāo)準(zhǔn)不同存在差異所以產(chǎn)生兼容性的問題。 瀏覽...
摘要:對象是一個值超出有效范圍時發(fā)生的錯誤。包括返回原數(shù)組包括數(shù)組對象函數(shù)可以用來判斷變量是否為對象數(shù)組對象函數(shù)構(gòu)造函數(shù)與直接賦值是等價的。只適用于,數(shù)組不適用通過可以看出一個值到底是什么類型,其中返回值的第二個值表示該值的構(gòu)造函數(shù)。 這是ES5的入門篇教程的筆記,網(wǎng)址:JavaScript教程,以下內(nèi)容中黑體表示大標(biāo)題,還有一些重點(diǎn);斜體表示對于自身,還需要下功夫?qū)W習(xí)的內(nèi)容。這里面有一些自...
摘要:另外自己寫代碼測試了下和的速度,比較結(jié)果如下位操作轉(zhuǎn)換整數(shù)的原理參考上面對于位操作的說明,點(diǎn)擊下面鏈接有這樣一段話中,數(shù)字存儲是雙進(jìn)度位浮點(diǎn)數(shù)。但是位操作卻會把要操作的運(yùn)算元當(dāng)做位帶符號的整數(shù)。因此進(jìn)行位操作時,會自動把數(shù)字先轉(zhuǎn)換為整數(shù)。 本文將會列舉并說明JavaScript 把一個number(或者numerical的對象)轉(zhuǎn)換成一個整數(shù)相關(guān)方法。 使用parseInt parse...
摘要:這里需要說明的是,小的整數(shù)對象,將全部直接放置于內(nèi)存中。內(nèi)存泄漏上述的機(jī)制可以很好減輕的問題,同時可以根據(jù)所跑的程序不同的特點(diǎn)來做從而編譯出自己認(rèn)為合適的。 墻上的斑點(diǎn) 我第一次注意到短褲上的那個破洞,大概是在金年的三月上旬。如果想要知道具體的時間,那就得回想一下當(dāng)時我看見的東西。我還能夠回憶起,游泳池頂上,搖曳的、白色的燈光不停地映在我的短褲上;有三五名少年一同扎進(jìn)了水里。哦,那是大...
閱讀 2250·2021-11-24 09:38
閱讀 3316·2021-11-08 13:27
閱讀 3153·2021-09-10 10:51
閱讀 3274·2019-08-29 12:20
閱讀 728·2019-08-28 18:28
閱讀 3518·2019-08-26 11:53
閱讀 2775·2019-08-26 11:46
閱讀 1586·2019-08-26 10:56