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