摘要:這里需要說明的是,小的整數(shù)對象,將全部直接放置于內(nèi)存中。內(nèi)存泄漏上述的機制可以很好減輕的問題,同時可以根據(jù)所跑的程序不同的特點來做從而編譯出自己認(rèn)為合適的。
“墻上的斑點”
我第一次注意到短褲上的那個破洞,大概是在金年的三月上旬。如果想要知道具體的時間,那就得回想一下當(dāng)時我看見的東西。我還能夠回憶起,游泳池頂上,搖曳的、白色的燈光不停地映在我的短褲上;有三五名少年一同扎進了水里。哦,那是大概是冬天,因為我回憶起當(dāng)時的前一天我和室友吃了部隊鍋,那段時間我沒有吸煙,反而嚼了許多口香糖,糖紙總是掉下去,無意中埋下頭,這是我第一次看到短褲上的那個破洞。
今天在機場等shuttle時,聽到旁邊的兩個年輕人神采飛揚地討論游泳的話題。莫名地回想起來,幾年前看了一篇講述Python內(nèi)部整數(shù)對象管理機制的文章,其中談到了Python應(yīng)用內(nèi)存池機制來對“大”整數(shù)對象進行管理。從它出發(fā),我想到了一些問題,想要在這里討論一下。
背景注:本文討論的Python版本是Python 2 (2.7.11),C實現(xiàn)。
“一切皆對象”我們知道,Python的對象,本質(zhì)上是C中的結(jié)構(gòu)體(生存于在系統(tǒng)堆上)。所有Python對象的根源都是PyObject這個結(jié)構(gòu)體。
打開Python源碼目錄的Include/,可以找到object.h這一文件,這一個文件,是整個Python對象機制的基礎(chǔ)。搜索PyObject,我們將會找到:
typedef struct _object { PyObject_HEAD } PyObject;
再看看PyObject_HEAD這個宏:
#define PyObject_HEAD _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; struct _typeobject *ob_type;
在實際編譯出的PyObject中,有ob_refcnt這個變量和ob_type這個指針。前者用于Python的引用計數(shù)垃圾收集,后者用于指定這個對象的“類型對象”。Python中可以把對象分為“普通”對象和類型對象。也就是說,表示對象的類型,是通過一個指針來指向另一個對象,即類型對象,來實現(xiàn)的。這是“一切皆對象”的一個關(guān)鍵體現(xiàn)。
Python中的整數(shù)對象Python里面,整數(shù)對象的頭文件intobject.h,也可以在Include/目錄里找到,這一文件定義了PyIntObject這一結(jié)構(gòu)體作為Python中的整數(shù)對象:
typedef struct { PyObject_HEAD long ob_ival; } PyIntObject;
上面提過了,每一個Python對象的ob_type都指向一個類型對象,這里PyIntObject則指向PyInt_Type。想要了解PyInt_Type的相關(guān)信息,我們可以打開intobject.c,并找到如下內(nèi)容:
PyTypeObject PyInt_Type = { PyObject_HEAD_INIT(&PyType_Type) 0, "int", sizeof(PyIntObject), 0, (destructor)int_dealloc, /* tp_dealloc */ (printfunc)int_print, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ (cmpfunc)int_compare, /* tp_compare */ (reprfunc)int_repr, /* tp_repr */ &int_as_number, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ (hashfunc)int_hash, /* tp_hash */ 0, /* tp_call */ (reprfunc)int_repr, /* tp_str */ PyObject_GenericGetAttr, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES | Py_TPFLAGS_BASETYPE, /* tp_flags */ int_doc, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ int_methods, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ 0, /* tp_alloc */ int_new, /* tp_new */ (freefunc)int_free, /* tp_free */ };
這里給Python的整數(shù)類型定義了許多的操作。拿int_dealloc,int_free,int_new這幾個操作舉例。顯而易見,int_dealloc負(fù)責(zé)析構(gòu),int_free負(fù)責(zé)釋放該對象所占用的內(nèi)存,int_new負(fù)責(zé)創(chuàng)建新的對象。int_as_number也是比較有意思的一個field。它指向一個PyNumberMethods結(jié)構(gòu)體。PyNumberMethods含有許多個函數(shù)指針,用以定義對數(shù)字的操作,比如加減乘除等等。
通用整數(shù)對象池Python里面,對象的創(chuàng)建一般是通過Python的C API或者是其類型對象。這里就不詳述具體的創(chuàng)建機制,具體內(nèi)容可以參考Python的有關(guān)文檔。這里我們想要關(guān)注的是,整數(shù)對象是如何存活在系統(tǒng)內(nèi)存中的。
整數(shù)對象大概會是常見Python程序中使用最頻繁的對象了。并且,正如上面提到過的,Python的一切皆對象而且對象都生存在系統(tǒng)的堆上,整數(shù)對象當(dāng)然不例外,那么以整數(shù)對象的使用頻度,系統(tǒng)堆將面臨難以想象的高頻的訪問。一些簡單的循環(huán)和計算,都會致使malloc和free一次次被調(diào)用,由此帶來的開銷是難以計數(shù)的。此外,heap也會有很多的fragmentation的情況,進一步導(dǎo)致性能下降。
這也是為什么通用整數(shù)對象池機制在Python中得到了應(yīng)用。這里需要說明的是,“小”的整數(shù)對象,將全部直接放置于內(nèi)存中。怎么樣定義“小”呢?繼續(xù)看intobject.c,我們可以看到:
#ifndef NSMALLPOSINTS #define NSMALLPOSINTS 257 #endif #ifndef NSMALLNEGINTS #define NSMALLNEGINTS 5 #endif #if NSMALLNEGINTS + NSMALLPOSINTS > 0 /* References to small integers are saved in this array so that they can be shared. The integers that are saved are those in the range -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive). */ static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
值在這個范圍內(nèi)的整數(shù)對象將被直接換存在內(nèi)存中,small_ints負(fù)責(zé)保存它們的指針。可以理解,這個數(shù)組越大,使用整數(shù)對象的性能(很可能)就越高。但是這里也是一個trade-off,畢竟系統(tǒng)內(nèi)存大小有限,直接緩存的小整數(shù)數(shù)量太多也會影響整體效率。
與小整數(shù)相對的是“大”整數(shù)對象,也就是除開小整數(shù)對象之外的其他整數(shù)對象。既然不可能再緩存所有,或者說大部分常用范圍的整數(shù),那么一個妥協(xié)的辦法就是提供一片空間讓這些大整數(shù)對象按需依次使用。Python也正是這么做的。它維護了兩個單向鏈表block_list和free_list。前者保存了許多被稱為PyIntBlock的結(jié)構(gòu),用于存儲被使用的大整數(shù)的PyIntObject;后者則用于維護前者所有block之中的空閑內(nèi)存。
仍舊是在intobject.c之中,我們可以看到:
struct _intblock { struct _intblock *next; PyIntObject objects[N_INTOBJECTS]; }; typedef struct _intblock PyIntBlock;
一個PyIntBlock保存N_INTOBJECTS個PyIntObject。
現(xiàn)在我們來思考一下一個Python整數(shù)對象在內(nèi)存中的“一生”。
被創(chuàng)建出來之前,先檢查其值的大小,如果在小整數(shù)的范圍內(nèi),則直接使用小整數(shù)池,只用更新其對應(yīng)整數(shù)對象的引用計數(shù)就可以了。如果是大整數(shù),則需要先檢查free_list看是否有空閑的空間,要是沒有則申請新的內(nèi)存空間,更新block_list和free_list,否則就使用free_list指向的下一個空閑內(nèi)存位置并且更新free_list。
“內(nèi)存泄漏”?So far so good. 上述的機制可以很好減輕fragmentation的問題,同時可以根據(jù)所跑的程序不同的特點來做fine tuning從而編譯出自己認(rèn)為合適的Python。但是我們只說了Python整數(shù)對象的“來”還沒有提它的“去”。當(dāng)一個整數(shù)對象的引用計數(shù)變成0以后,會發(fā)生什么事情呢?
小整數(shù)對象自是不必?fù)?dān)心,始終都是在內(nèi)存中的;大整數(shù)對象則需要調(diào)用析構(gòu)操作,int_dealloc (intobject.c):
static void int_dealloc(PyIntObject *v) { if (PyInt_CheckExact(v)) { Py_TYPE(v) = (struct _typeobject *)free_list; free_list = v; } else Py_TYPE(v)->tp_free((PyObject *)v); }
這個PyInt_CheckExact,來自于intobject.h:
#define PyInt_CheckExact(op) ((op)->ob_type == &PyInt_Type)
它起到了類型檢查的作用。所以如果這個指針v指向的不是Python原生整數(shù)對象,則int_dealloc直接調(diào)用該類型的tp_free操作;否則把不再需要的那塊內(nèi)存放入free_list之中。
Py_TYPE的定義:
#ifndef Py_TYPE #define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) #endif
可以看出,free_list所維護的單向鏈表,是使用ob_type這個field來鏈接前后元素的。
這也就是說,當(dāng)一個大整數(shù)PyIntObject的生命周期結(jié)束時,它之前的內(nèi)存不會交換給系統(tǒng)堆,而是通過free_list繼續(xù)被該Python進程占有。
倘若一個程序使用很多的大整數(shù)呢?倘若每個大整數(shù)只被使用一次呢?是不是很像內(nèi)存泄漏?
我們來做個簡單的計算,假如你的電腦是Macbook Air,8GB Memory,如果你的PyIntObject占用24個Byte,那么滿打滿算,能夠存下大約357913941個整數(shù)對象。
下面做個實驗。以下程序運行在Macbook Pro (mid 2015), 2.5Ghz i7, 16 GB Memory,Python 2.7.11的環(huán)境下:
l = list() num = 178956971 for i in range(0, num): l.append(i) if len(l) % 100000 == 0: l[:] = []
運行這個程序,會發(fā)現(xiàn)它占用了5.44GB的內(nèi)存:
如果把整數(shù)個數(shù)減半,比如使用89478486,則會占用2.72GB內(nèi)存(正好原來一半):
一個PyIntObject占用多大內(nèi)存呢?
講道理,24 bytes x 178956971 = 4294967304 bytes,約等于2^32,也就是4GB,那么為什么會占用5.44GB呢?
這并非程序其他部分的overhead,因為,就算你的程序只含有:
for i in range(0, 178956971): pass
它仍舊會占用5.44GB內(nèi)存。5.44 x 2^30 / 178956971大約等于32.64,也就是均攤下來一個整數(shù)對象占用了32.64個Byte.
這個問題可以作為一個簡單的思考題,這里就不討論了。
總結(jié)Python的整數(shù)對象管理機制并不復(fù)雜,但也有趣,剛接觸Python的時候是很好的學(xué)習(xí)材料。細(xì)糾下來會發(fā)現(xiàn)有很多工程上的考慮以及與之相關(guān)的現(xiàn)象,值得我們深入挖掘。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/38233.html
摘要:魔法方法類構(gòu)造方法魔法方法初始化對象創(chuàng)建對象的過程創(chuàng)建一個對象解釋器會自動的調(diào)用方法返回創(chuàng)建的對象的引用,給實例實例化執(zhí)行該方法,返回值。當(dāng)引用計數(shù)為時,該對象生命就結(jié)束了。 define class class的三個組成部分: 類的名稱:類名 類的屬性: 一組數(shù)據(jù) 類的方法:允許對進行操作的方法(行為) 定義 class Student (object): pass...
摘要:編譯參見深入理解虛擬機節(jié)走進之一自己編譯源碼內(nèi)存模型運行時數(shù)據(jù)區(qū)域根據(jù)虛擬機規(guī)范的規(guī)定,的內(nèi)存包括以下幾個運運行時數(shù)據(jù)區(qū)域程序計數(shù)器程序計數(shù)器是一塊較小的內(nèi)存空間,他可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。 點擊進入我的博客 1.1 基礎(chǔ)知識 1.1.1 一些基本概念 JDK(Java Development Kit):Java語言、Java虛擬機、Java API類庫JRE(...
摘要:錯誤使用單利在開發(fā)中單例經(jīng)常需要持有對象,如果持有的對象生命周期與單例生命周期更短時,或?qū)е聼o法被釋放回收,則有可能造成內(nèi)存泄漏。如果集合是類型的話,那內(nèi)存泄漏情況就會更為嚴(yán)重。 目錄介紹 1.OOM和崩潰優(yōu)化 1.1 OOM優(yōu)化 1.2 ANR優(yōu)化 1.3 Crash優(yōu)化 2.內(nèi)存泄漏優(yōu)化 2.0 動畫資源未釋放 2.1 錯誤使用單利 2.2 錯誤使用靜態(tài)變量 2.3 ...
摘要:這是因為我們訪問了數(shù)組中不存在的數(shù)組元素它超過了最后一個實際分配到內(nèi)存的數(shù)組元素字節(jié),并且有可能會讀取或者覆寫的位。包含個元素的新數(shù)組由和數(shù)組元素所組成中的內(nèi)存使用中使用分配的內(nèi)存主要指的是內(nèi)存讀寫。 原文請查閱這里,本文有進行刪減,文后增了些經(jīng)驗總結(jié)。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第三章。 我們將會討論日常使用中另一個被開發(fā)...
閱讀 3413·2021-11-25 09:43
閱讀 3468·2021-11-19 09:40
閱讀 2470·2021-10-14 09:48
閱讀 1285·2021-09-09 11:39
閱讀 1925·2019-08-30 15:54
閱讀 2826·2019-08-30 15:44
閱讀 2001·2019-08-29 13:12
閱讀 1546·2019-08-29 12:59