Python進(jìn)階2-元組和列表的內(nèi)存分配機(jī)制

本系列文章是一系列學(xué)習(xí)筆記籍茧,希望較為深入地分析Python3中的原理肮塞、性能襟齿,文章中絕大部分觀點(diǎn)都是原作作者的觀點(diǎn)(如下),本人對(duì)書中示例加以實(shí)踐和總結(jié)枕赵,并結(jié)合相應(yīng)的Python的C語言源碼(3.6.1)猜欺,分享出來。原著:

  • 《High Performance Python》by O'Relly Media烁设,作者M(jìn)icha Gorelick替梨,Ian Ozsvald
  • 《Fluent Python》by O'Relly Media,作者Luciano Ramalho

從內(nèi)存利用和CPU利用開始了解List和Tuple的優(yōu)缺點(diǎn)

定義

List:動(dòng)態(tài)數(shù)組装黑,元素可變副瀑,可改變大小(append恋谭,resize)
Tuple:靜態(tài)數(shù)組糠睡,不可變,數(shù)據(jù)一旦創(chuàng)建后不可改變

List的內(nèi)存利用

  • 當(dāng)創(chuàng)建N個(gè)元素的List時(shí)疚颊,Python的動(dòng)態(tài)內(nèi)存分配長N+1個(gè)元素的內(nèi)存狈孔,第一個(gè)元素存儲(chǔ)列表長度,和列表的元信息材义。
  • 當(dāng)Append一個(gè)元素時(shí)均抽,Python將創(chuàng)建一個(gè)足夠大的列表,來容納N個(gè)元素和將要被追加的元素其掂。重新創(chuàng)建的列表長度大于N+1(雖然我們只觸發(fā)一次append操作)油挥,實(shí)際上,為了未來的Append操作款熬,M個(gè)元素長度(M>N+1)的內(nèi)存將會(huì)被額外分配深寥,然后,舊列表中的數(shù)據(jù)被copy到新列表中贤牛,舊列表銷毀惋鹅。
  • 額外內(nèi)存的分配,只會(huì)發(fā)生在第一次Append操作時(shí)殉簸,當(dāng)我們創(chuàng)建普通列表時(shí)闰集,不會(huì)額外分配內(nèi)存沽讹。
  • 這里的哲學(xué)是,一個(gè)Append操作很可能是很多Append操作的開始返十,通過額外分配內(nèi)存來減少可能的內(nèi)存分配和內(nèi)存copy的次數(shù)妥泉。
  • 那么,對(duì)于一個(gè)具有N個(gè)元素的列表洞坑,當(dāng)一次Append操作發(fā)生時(shí)盲链,新列表要分配多少內(nèi)存(額外M個(gè)元素,需多分配一個(gè)元素存儲(chǔ)長度)呢迟杂?答案是:

** M = (N >> 3) + (N <9 ? 3 : 6) + 1 **

我們來看Python3.6.1的列表resize過程刽沾,源代碼位于Objects/listobject.c中的list_resize函數(shù):

static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{
    PyObject **items;
    size_t new_allocated;
    Py_ssize_t allocated = self->allocated;

    /* Bypass realloc() when a previous overallocation is large enough
       to accommodate the newsize.  If the newsize falls lower than half
       the allocated size, then proceed with the realloc() to shrink the list.
    */
    if (allocated >= newsize && newsize >= (allocated >> 1)) {
        assert(self->ob_item != NULL || newsize == 0);
        Py_SIZE(self) = newsize;
        return 0;
    }

    /* This over-allocates proportional to the list size, making room
     * for additional growth.  The over-allocation is mild, but is
     * enough to give linear-time amortized behavior over a long
     * sequence of appends() in the presence of a poorly-performing
     * system realloc().
     * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
     */
    new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6);

    /* check for integer overflow */
    if (new_allocated > SIZE_MAX - newsize) {
        PyErr_NoMemory();
        return -1;
    } else {
        new_allocated += newsize;
    }

    if (newsize == 0)
        new_allocated = 0;
    items = self->ob_item;
    if (new_allocated <= (SIZE_MAX / sizeof(PyObject *)))
        PyMem_RESIZE(items, PyObject *, new_allocated);
    else
        items = NULL;
    if (items == NULL) {
        PyErr_NoMemory();
        return -1;
    }
    self->ob_item = items;
    Py_SIZE(self) = newsize;
    self->allocated = new_allocated;
    return 0;
}

結(jié)合C源碼我們來舉個(gè)例子,當(dāng)一個(gè)list長度為8時(shí)排拷,發(fā)生append操作后:
1)new_size = 原有的size + append一個(gè)對(duì)象 = 8 + 1 = 9
2)newsize為9侧漓,二進(jìn)制是 1001,9 >> 3 = 1
3)new_allocated = 9 >> 3 + 6 = 7
4)new_allocated += new_size监氢,為9 + 7 = 16
4)列表的最終大小為Py_SIZE = 16


Tuple的內(nèi)存利用

  • 雖然Tuple不支持resize布蔗,但是我們可以粘貼兩個(gè)元祖組成一個(gè)新的元組,這個(gè)操作類似于List的append浪腐,但是又不會(huì)額外的分配內(nèi)存纵揍。但我們不能把它當(dāng)成append,因?yàn)槊看味紩?huì)進(jìn)行一個(gè)分配內(nèi)存和內(nèi)存copy操作议街。
  • 另一個(gè)Tuple的靜態(tài)本質(zhì)帶來的好處是泽谨,resource caching。Python是garbage collected特漩,當(dāng)一個(gè)變量不用了吧雹,內(nèi)存會(huì)被回收并交回給OS。但是涂身,對(duì)于一個(gè)20個(gè)元素的Tuple雄卷,當(dāng)它不再被用時(shí),內(nèi)存不會(huì)立即返還給OS蛤售,而是為了以后應(yīng)用而暫緩保留龙亲,當(dāng)一個(gè)新的Tuple被創(chuàng)建時(shí),我們不會(huì)向OS重新申請(qǐng)分配內(nèi)存悍抑,而是用現(xiàn)有reserved的free memory。
  • 也就是杜耙,Tuple的創(chuàng)建很簡單并且避免頻繁與OS申請(qǐng)內(nèi)存搜骡,創(chuàng)建一個(gè)具有10個(gè)元素的Tuple比創(chuàng)建一個(gè)List要快不少,55ns VS 280 ns佑女。

我們可以通過Python源碼看到上面的結(jié)論记靡,代碼位于Objects/tupleobject.c谈竿,我們可以清楚的看到tuple的粘貼過程:

  • 新的大小等于兩個(gè)tuple大小之和
  • 重新分配內(nèi)存
  • 對(duì)于分配好的新內(nèi)存,通過兩個(gè)for循環(huán)將原來的兩個(gè)元組拷貝到新的元組上
static PyObject *
tupleconcat(PyTupleObject *a, PyObject *bb)
{
    Py_ssize_t size;
    Py_ssize_t i;
    PyObject **src, **dest;
    PyTupleObject *np;
    if (!PyTuple_Check(bb)) {
        PyErr_Format(PyExc_TypeError,
             "can only concatenate tuple (not \"%.200s\") to tuple",
                 Py_TYPE(bb)->tp_name);
        return NULL;
    }
#define b ((PyTupleObject *)bb)
    if (Py_SIZE(a) > PY_SSIZE_T_MAX - Py_SIZE(b))
        return PyErr_NoMemory();
    size = Py_SIZE(a) + Py_SIZE(b);
    np = (PyTupleObject *) PyTuple_New(size);
    if (np == NULL) {
        return NULL;
    }
    src = a->ob_item;
    dest = np->ob_item;
    for (i = 0; i < Py_SIZE(a); i++) {
        PyObject *v = src[i];
        Py_INCREF(v);
        dest[i] = v;
    }
    src = b->ob_item;
    dest = np->ob_item + Py_SIZE(a);
    for (i = 0; i < Py_SIZE(b); i++) {
        PyObject *v = src[i];
        Py_INCREF(v);
        dest[i] = v;
    }
    return (PyObject *)np;
#undef b
}
  • 在分配內(nèi)存函數(shù)PyTuple_New中摸吠,當(dāng)大小小于20時(shí)空凸,Python會(huì)直接從一個(gè)空閑的內(nèi)存表中拿出來,不會(huì)重新申請(qǐng)寸痢,這減少了小元組的內(nèi)存訪問次數(shù)呀洲,宏P(guān)yTuple_MAXSAVESIZE為20
PyObject *
PyTuple_New(Py_ssize_t size)
{
    PyTupleObject *op;
    Py_ssize_t i;
    if (size < 0) {
        PyErr_BadInternalCall();
        return NULL;
    }
#if PyTuple_MAXSAVESIZE > 0
    if (size == 0 && free_list[0]) {
        op = free_list[0];
        Py_INCREF(op);
#ifdef COUNT_ALLOCS
        tuple_zero_allocs++;
#endif
        return (PyObject *) op;
    }
    if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) {
        free_list[size] = (PyTupleObject *) op->ob_item[0];
        numfree[size]--;
#ifdef COUNT_ALLOCS
        fast_tuple_allocs++;
#endif
        /* Inline PyObject_InitVar */
#ifdef Py_TRACE_REFS
        Py_SIZE(op) = size;
        Py_TYPE(op) = &PyTuple_Type;
#endif
        _Py_NewReference((PyObject *)op);
    }
    else
#endif
    {
        /* Check for overflow */
        if ((size_t)size > ((size_t)PY_SSIZE_T_MAX - sizeof(PyTupleObject) -
                    sizeof(PyObject *)) / sizeof(PyObject *)) {
            return PyErr_NoMemory();
        }
        op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
        if (op == NULL)
            return NULL;
    }
    for (i=0; i < size; i++)
        op->ob_item[i] = NULL;
#if PyTuple_MAXSAVESIZE > 0
    if (size == 0) {
        free_list[0] = op;
        ++numfree[0];
        Py_INCREF(op);          /* extra INCREF so that this is never freed */
    }
#endif
#ifdef SHOW_TRACK_COUNT
    count_tracked++;
#endif
    _PyObject_GC_TRACK(op);
    return (PyObject *) op;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市啼止,隨后出現(xiàn)的幾起案子道逗,更是在濱河造成了極大的恐慌,老刑警劉巖献烦,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滓窍,死亡現(xiàn)場離奇詭異,居然都是意外死亡巩那,警方通過查閱死者的電腦和手機(jī)吏夯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來即横,“玉大人噪生,你說我怎么就攤上這事×罹常” “怎么了杠园?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舔庶。 經(jīng)常有香客問我抛蚁,道長,這世上最難降的妖魔是什么惕橙? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任瞧甩,我火速辦了婚禮,結(jié)果婚禮上弥鹦,老公的妹妹穿的比我還像新娘肚逸。我一直安慰自己,他們只是感情好彬坏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布朦促。 她就那樣靜靜地躺著,像睡著了一般栓始。 火紅的嫁衣襯著肌膚如雪务冕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天幻赚,我揣著相機(jī)與錄音禀忆,去河邊找鬼臊旭。 笑死,一個(gè)胖子當(dāng)著我的面吹牛箩退,可吹牛的內(nèi)容都是我干的离熏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼戴涝,長吁一口氣:“原來是場噩夢啊……” “哼滋戳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起喊括,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤胧瓜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后郑什,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體府喳,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蘑拯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钝满。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡申窘,死狀恐怖弯蚜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剃法,我是刑警寧澤碎捺,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站贷洲,受9級(jí)特大地震影響收厨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜优构,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一诵叁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钦椭,春花似錦拧额、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至德挣,卻和暖如春捎拯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工署照, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吗浩。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓建芙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親懂扼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子禁荸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • http://python.jobbole.com/85231/ 關(guān)于專業(yè)技能寫完項(xiàng)目接著寫寫一名3年工作經(jīng)驗(yàn)的J...
    燕京博士閱讀 7,579評(píng)論 1 118
  • 1、字符串的遍歷 ES6為字符串添加了遍歷接口阀湿,使得字符串可以被 for...of 循環(huán)遍歷赶熟。 2、include...
    開車去環(huán)游世界閱讀 239評(píng)論 0 0
  • 我喜歡一個(gè)人的旅行陷嘴,閑暇的時(shí)間總會(huì)一個(gè)人搭上飛機(jī)映砖,火車,或著大巴車去遙遠(yuǎn)的地方灾挨,那是我生命中向往的一種樂趣邑退。 很多...
    擎晨馬春燕閱讀 661評(píng)論 0 2
  • 我喜歡這本書的故事風(fēng)格。就像一個(gè)玲瓏的小男孩坐在床頭劳澄,慢悠悠的跟你講故事地技,燭光搖曳,咖啡的濃香鋪滿整個(gè)房間秒拔,被子溫...
    千影鹿閱讀 1,577評(píng)論 4 24
  • 楚門的世界莫矗,和大部分的電影不太一樣,這部電影好像是類似于戲中戲砂缩,但又不是戲中戲作谚。 主人公出門生后在意的普通的地區(qū),...
    王澤宇_閱讀 739評(píng)論 0 2