เหตุใดสิ่งที่เพิ่มขึ้นจึงใช้พื้นที่ในหน่วยความจำน้อยกว่ารายการ


105

A tupleใช้พื้นที่หน่วยความจำน้อยลงใน Python:

>>> a = (1,2,3)
>>> a.__sizeof__()
48

ในขณะที่lists ใช้พื้นที่หน่วยความจำมากกว่า:

>>> b = [1,2,3]
>>> b.__sizeof__()
64

เกิดอะไรขึ้นภายในการจัดการหน่วยความจำ Python


1
ฉันไม่แน่ใจว่าสิ่งนี้ทำงานอย่างไรภายใน แต่อย่างน้อยวัตถุรายการก็มีฟังก์ชั่นเพิ่มเติมเช่นตัวอย่างต่อท้ายซึ่งทูเปิลไม่มี ดังนั้นจึงสมเหตุสมผลสำหรับทูเปิลในฐานะวัตถุประเภทที่เรียบง่ายกว่าที่จะมีขนาดเล็กลง
Metareven

ฉันคิดว่ามันขึ้นอยู่กับเครื่องต่อเครื่องด้วย .... สำหรับฉันเมื่อฉันตรวจสอบ a = (1,2,3) ใช้เวลา 72 และ b = [1,2,3] ใช้เวลา 88
Amrit

6
สิ่งที่เพิ่มขึ้นของ Python ไม่เปลี่ยนรูป วัตถุที่เปลี่ยนแปลงได้มีค่าใช้จ่ายพิเศษในการจัดการกับการเปลี่ยนแปลงรันไทม์
Lee Daniel Crocker

@ กำหนดจำนวนเมธอดประเภทหนึ่งไม่ส่งผลกระทบต่อพื้นที่หน่วยความจำที่อินสแตนซ์ใช้ รายการวิธีการและรหัสของพวกเขาได้รับการจัดการโดยต้นแบบออบเจ็กต์ แต่อินสแตนซ์จะเก็บเฉพาะข้อมูลและตัวแปรภายใน
jjmontes

คำตอบ:


145

ฉันคิดว่าคุณกำลังใช้ CPython และด้วย 64 บิต (ฉันได้ผลลัพธ์เดียวกันกับ CPython 2.7 64 บิตของฉัน) อาจมีความแตกต่างในการใช้งาน Python อื่น ๆ หรือหากคุณมี Python 32 บิต

ไม่ว่าจะใช้งานอย่างไรlists จะมีขนาดแปรผันในขณะที่tuples เป็นขนาดคงที่

ดังนั้นtuples สามารถจัดเก็บองค์ประกอบได้โดยตรงภายในโครงสร้างในทางกลับกันรายการจำเป็นต้องมีเลเยอร์ของทิศทาง (มันเก็บตัวชี้ไปยังองค์ประกอบ) เลเยอร์ของทิศทางนี้เป็นตัวชี้บนระบบ 64 บิตที่เป็น 64 บิตดังนั้น 8 ไบต์

แต่มีอีกสิ่งหนึ่งที่listทำ: พวกเขาจัดสรรมากเกินไป มิฉะนั้นlist.appendจะเป็นการO(n)ดำเนินการเสมอ - เพื่อให้ตัดจำหน่ายO(1)(เร็วขึ้นมาก !!!) มันเกินจัดสรร แต่ตอนนี้ต้องติดตามขนาดที่จัดสรรและขนาดที่เติม ( tupleต้องจัดเก็บเพียงขนาดเดียวเนื่องจากขนาดที่จัดสรรและขนาดที่เติมจะเหมือนกันเสมอ) นั่นหมายความว่าแต่ละรายการต้องจัดเก็บ "ขนาด" อื่นซึ่งในระบบ 64 บิตเป็นจำนวนเต็ม 64 บิตและ 8 ไบต์อีกครั้ง

ดังนั้นlistต้องมีหน่วยความจำมากกว่าtuples อย่างน้อย 16 ไบต์ ทำไมฉันถึงพูดว่า "อย่างน้อย"? เนื่องจากการจัดสรรที่มากเกินไป การจัดสรรมากเกินไปหมายถึงการจัดสรรพื้นที่มากกว่าที่จำเป็น อย่างไรก็ตามจำนวนการจัดสรรส่วนเกินขึ้นอยู่กับ "วิธี" ที่คุณสร้างรายการและประวัติการผนวก / ลบ:

>>> l = [1,2,3]
>>> l.__sizeof__()
64
>>> l.append(4)  # triggers re-allocation (with over-allocation), because the original list is full
>>> l.__sizeof__()
96

>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1)  # re-allocation with over-allocation
>>> l.__sizeof__()
72
>>> l.append(2)  # no re-alloc
>>> l.append(3)  # no re-alloc
>>> l.__sizeof__()
72
>>> l.append(4)  # still has room, so no over-allocation needed (yet)
>>> l.__sizeof__()
72

รูปภาพ

ฉันตัดสินใจสร้างภาพเพื่อประกอบคำอธิบายด้านบน สิ่งเหล่านี้อาจเป็นประโยชน์

นี่คือวิธีจัดเก็บ (แผนผัง) ไว้ในหน่วยความจำในตัวอย่างของคุณ ฉันเน้นความแตกต่างด้วยรอบสีแดง (มือเปล่า):

ป้อนคำอธิบายภาพที่นี่

นั่นเป็นเพียงการประมาณเนื่องจากintวัตถุยังเป็นวัตถุ Python และ CPython ยังนำจำนวนเต็มขนาดเล็กมาใช้ซ้ำดังนั้นการแทนค่าที่แม่นยำกว่า (แม้ว่าจะอ่านไม่ได้) ของวัตถุในหน่วยความจำจะเป็น:

ป้อนคำอธิบายภาพที่นี่

ลิงค์ที่เป็นประโยชน์:

โปรดทราบ__sizeof__ว่าไม่ได้ส่งคืนขนาด "ที่ถูกต้อง" จริงๆ! ส่งคืนเฉพาะขนาดของค่าที่เก็บไว้ อย่างไรก็ตามเมื่อคุณใช้sys.getsizeofผลลัพธ์จะแตกต่างกัน:

>>> import sys
>>> l = [1,2,3]
>>> t = (1, 2, 3)
>>> sys.getsizeof(l)
88
>>> sys.getsizeof(t)
72

มี 24 ไบต์ "พิเศษ" สิ่งเหล่านี้เป็นเรื่องจริงนั่นคือค่าใช้จ่ายของตัวเก็บขยะที่ไม่ได้คิดไว้ใน__sizeof__วิธีการนี้ นั่นเป็นเพราะโดยทั่วไปคุณไม่ควรใช้เมธอดมายากลโดยตรง - ใช้ฟังก์ชันที่รู้วิธีจัดการในกรณีนี้: sys.getsizeof(ซึ่งจริง ๆ แล้วจะเพิ่มค่าโสหุ้ย GCไปยังค่าที่ส่งกลับมา__sizeof__)


Re " ดังนั้นรายการต้องมีหน่วยความจำมากกว่าสิ่งที่ทูเปิลอย่างน้อย 16 ไบต์ " นั่นจะไม่เท่ากับ 8 หรือ หนึ่งขนาดสำหรับทูเปิลและสองขนาดสำหรับรายการหมายถึงขนาดพิเศษสำหรับรายการ
ikegami

1
ใช่รายการมี "ขนาด" พิเศษ (8 ไบต์) หนึ่งตัว แต่ยังเก็บตัวชี้ (8 ไบต์) ไว้ที่ "อาร์เรย์ของ PyObject" แทนที่จะเก็บไว้ในโครงสร้างโดยตรง (สิ่งที่ทูเปิลทำ) 8 + 8 = 16.
MSeifert

2
อีกลิงค์ที่มีประโยชน์เกี่ยวกับการlistจัดสรรหน่วยความจำstackoverflow.com/questions/40018398/…
vishes_shell

@vishes_shell ไม่เกี่ยวข้องกับคำถามจริงๆเนื่องจากโค้ดในคำถามไม่ได้จัดสรรมากเกินไป แต่ใช่ว่าจะมีประโยชน์ในกรณีที่คุณต้องการทราบข้อมูลเพิ่มเติมเกี่ยวกับจำนวนการจัดสรรมากเกินไปเมื่อใช้งานlist()หรือความเข้าใจในรายการ
MSeifert

1
@ user3349993 ทูเปิลไม่เปลี่ยนรูปดังนั้นคุณจึงไม่สามารถต่อท้ายทูเพิลหรือลบไอเท็มออกจากทูเพิลได้
MSeifert

31

ฉันจะเจาะลึกเกี่ยวกับ CPython codebase เพื่อที่เราจะได้เห็นว่าขนาดต่างๆถูกคำนวณอย่างไร ในตัวอย่างเฉพาะของคุณ , ไม่เกินจัดสรร-ได้รับการดำเนินการดังนั้นฉันจะไม่สัมผัสกับว่า

ฉันจะใช้ค่า 64 บิตที่นี่อย่างที่คุณเป็น


ขนาดlistของ s คำนวณจากฟังก์ชันต่อไปนี้list_sizeof:

static PyObject *
list_sizeof(PyListObject *self)
{
    Py_ssize_t res;

    res = _PyObject_SIZE(Py_TYPE(self)) + self->allocated * sizeof(void*);
    return PyInt_FromSsize_t(res);
}

นี่Py_TYPE(self)คือมาโครที่จับob_typeของself(ส่งคืนPyList_Type) ในขณะที่ _PyObject_SIZEเป็นมาโครอื่นที่ดึงมาtp_basicsizeจากประเภทนั้น tp_basicsizeจะถูกคำนวณเป็นsizeof(PyListObject)ที่PyListObjectเป็น struct อินสแตนซ์

PyListObjectโครงสร้างมีสามสาขา:

PyObject_VAR_HEAD     # 24 bytes 
PyObject **ob_item;   #  8 bytes
Py_ssize_t allocated; #  8 bytes

สิ่งเหล่านี้มีความคิดเห็น (ซึ่งฉันตัดทอน) อธิบายว่าคืออะไรไปที่ลิงค์ด้านบนเพื่ออ่าน PyObject_VAR_HEADขยายเป็นสามเขต 8 ไบต์ ( ob_refcount, ob_typeและob_size) เพื่อให้24มีส่วนร่วมไบต์

ตอนนี้resคือ:

sizeof(PyListObject) + self->allocated * sizeof(void*)

หรือ:

40 + self->allocated * sizeof(void*)

หากอินสแตนซ์รายการมีองค์ประกอบที่จัดสรร ส่วนที่สองคำนวณการมีส่วนร่วมของพวกเขา self->allocatedตามความหมายของชื่อถือจำนวนองค์ประกอบที่จัดสรร

ไม่มีองค์ประกอบใด ๆ ขนาดของรายการจะถูกคำนวณให้เป็น:

>>> [].__sizeof__()
40

เช่นขนาดของโครงสร้างอินสแตนซ์


tupleวัตถุไม่ได้กำหนดtuple_sizeofฟังก์ชัน พวกเขาใช้object_sizeofคำนวณขนาดของมันแทน:

static PyObject *
object_sizeof(PyObject *self, PyObject *args)
{
    Py_ssize_t res, isize;

    res = 0;
    isize = self->ob_type->tp_itemsize;
    if (isize > 0)
        res = Py_SIZE(self) * isize;
    res += self->ob_type->tp_basicsize;

    return PyInt_FromSsize_t(res);
}

นี้เป็นสำหรับlistS, คว้าtp_basicsizeและถ้าวัตถุมีไม่ใช่ศูนย์tp_itemsize(หมายถึงมันมีกรณีตัวแปรความยาว) ก็คูณจำนวนของรายการใน tuple (ซึ่งจะได้รับผ่านทางPy_SIZE) tp_itemsizeด้วย

tp_basicsizeใช้อีกครั้งโดยsizeof(PyTupleObject)ที่โครงสร้าง PyTupleObjectประกอบด้วย :

PyObject_VAR_HEAD       # 24 bytes 
PyObject *ob_item[1];   # 8  bytes

ดังนั้นหากไม่มีองค์ประกอบใด ๆ (นั่นคือPy_SIZEผลตอบแทน0) ขนาดของสิ่งที่ว่างเปล่าจะเท่ากับsizeof(PyTupleObject):

>>> ().__sizeof__()
24

ฮะ? นี่คือความแปลกที่ฉันไม่พบคำอธิบายtp_basicsizeของtuples คำนวณได้ดังนี้:

sizeof(PyTupleObject) - sizeof(PyObject *)

ทำไม8ไบต์เพิ่มเติมจึงถูกลบออกtp_basicsizeจึงเป็นสิ่งที่ฉันไม่สามารถหาได้ (ดูความคิดเห็นของ MSeifert สำหรับคำอธิบายที่เป็นไปได้)


แต่นี้เป็นพื้นแตกต่างในตัวอย่างของคุณโดยเฉพาะ listนอกจากนี้ยังเก็บองค์ประกอบที่จัดสรรไว้จำนวนหนึ่งซึ่งช่วยในการกำหนดว่าเมื่อใดจะจัดสรรมากเกินไปอีกครั้ง

ตอนนี้เมื่อมีการเพิ่มองค์ประกอบเพิ่มเติมรายการจะดำเนินการจัดสรรส่วนเกินนี้เพื่อให้บรรลุ O (1) ต่อท้าย ส่งผลให้มีขนาดใหญ่ขึ้นเนื่องจาก MSeifert ครอบคลุมคำตอบของเขาเป็นอย่างดี


ฉันเชื่อว่าob_item[1]ส่วนใหญ่เป็นตัวยึดตำแหน่ง (ดังนั้นจึงเหมาะสมที่จะหักออกจากขนาดพื้นฐาน) มีการจัดสรรการใช้tuple PyObject_NewVarฉันยังไม่ได้หารายละเอียดดังนั้นนั่นเป็นเพียงการคาดเดาที่มีการศึกษา ...
MSeifert

@MSeifert ขออภัยที่แก้ไข :-) ฉันไม่รู้จริงๆฉันจำได้ว่าเคยพบมันในอดีตในบางช่วงเวลา แต่ฉันไม่เคยให้ความสนใจมากเกินไปบางทีฉันอาจจะถามคำถามในบางประเด็นในอนาคต :-)
Dimitris Fasarakis Hilliard

29

คำตอบของ MSeifert ครอบคลุมกว้าง ๆ เพื่อให้ง่ายขึ้นคุณสามารถนึกถึง:

tupleไม่เปลี่ยนรูป เมื่อตั้งค่าแล้วคุณจะไม่สามารถเปลี่ยนแปลงได้ คุณจึงรู้ล่วงหน้าว่าคุณต้องจัดสรรหน่วยความจำให้กับวัตถุนั้นมากแค่ไหน

listไม่แน่นอน คุณสามารถเพิ่มหรือลบรายการเข้าหรือออกได้ มันต้องรู้ขนาดของมัน (สำหรับการบอกต่อภายใน) มันปรับขนาดได้ตามต้องการ

ไม่มีอาหารฟรี - ความสามารถเหล่านี้มาพร้อมกับค่าใช้จ่าย ดังนั้นค่าใช้จ่ายในหน่วยความจำสำหรับรายการ


3

ขนาดของทูเปิลเป็นคำนำหน้าซึ่งหมายความว่าในการเริ่มต้นทูเปิลล่ามจะจัดสรรพื้นที่เพียงพอสำหรับข้อมูลที่มีอยู่และนั่นคือจุดสิ้นสุดของมันทำให้ไม่เปลี่ยนรูป (ไม่สามารถแก้ไขได้) ในขณะที่รายการเป็นวัตถุที่เปลี่ยนแปลงไม่ได้ดังนั้นจึงหมายถึงไดนามิก การจัดสรรหน่วยความจำดังนั้นเพื่อหลีกเลี่ยงการจัดสรรพื้นที่ทุกครั้งที่คุณต่อท้ายหรือแก้ไขรายการ (จัดสรรพื้นที่ให้เพียงพอที่จะมีข้อมูลที่เปลี่ยนแปลงและคัดลอกข้อมูลไป) มันจะจัดสรรพื้นที่เพิ่มเติมสำหรับการผนวกการแก้ไขในอนาคต ... สรุปได้

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.