A tuple
ใช้พื้นที่หน่วยความจำน้อยลงใน Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
ในขณะที่list
s ใช้พื้นที่หน่วยความจำมากกว่า:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
เกิดอะไรขึ้นภายในการจัดการหน่วยความจำ Python
A tuple
ใช้พื้นที่หน่วยความจำน้อยลงใน Python:
>>> a = (1,2,3)
>>> a.__sizeof__()
48
ในขณะที่list
s ใช้พื้นที่หน่วยความจำมากกว่า:
>>> b = [1,2,3]
>>> b.__sizeof__()
64
เกิดอะไรขึ้นภายในการจัดการหน่วยความจำ Python
คำตอบ:
ฉันคิดว่าคุณกำลังใช้ CPython และด้วย 64 บิต (ฉันได้ผลลัพธ์เดียวกันกับ CPython 2.7 64 บิตของฉัน) อาจมีความแตกต่างในการใช้งาน Python อื่น ๆ หรือหากคุณมี Python 32 บิต
ไม่ว่าจะใช้งานอย่างไรlist
s จะมีขนาดแปรผันในขณะที่tuple
s เป็นขนาดคงที่
ดังนั้นtuple
s สามารถจัดเก็บองค์ประกอบได้โดยตรงภายในโครงสร้างในทางกลับกันรายการจำเป็นต้องมีเลเยอร์ของทิศทาง (มันเก็บตัวชี้ไปยังองค์ประกอบ) เลเยอร์ของทิศทางนี้เป็นตัวชี้บนระบบ 64 บิตที่เป็น 64 บิตดังนั้น 8 ไบต์
แต่มีอีกสิ่งหนึ่งที่list
ทำ: พวกเขาจัดสรรมากเกินไป มิฉะนั้นlist.append
จะเป็นการO(n)
ดำเนินการเสมอ - เพื่อให้ตัดจำหน่ายO(1)
(เร็วขึ้นมาก !!!) มันเกินจัดสรร แต่ตอนนี้ต้องติดตามขนาดที่จัดสรรและขนาดที่เติม ( tuple
ต้องจัดเก็บเพียงขนาดเดียวเนื่องจากขนาดที่จัดสรรและขนาดที่เติมจะเหมือนกันเสมอ) นั่นหมายความว่าแต่ละรายการต้องจัดเก็บ "ขนาด" อื่นซึ่งในระบบ 64 บิตเป็นจำนวนเต็ม 64 บิตและ 8 ไบต์อีกครั้ง
ดังนั้นlist
ต้องมีหน่วยความจำมากกว่าtuple
s อย่างน้อย 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 ยังนำจำนวนเต็มขนาดเล็กมาใช้ซ้ำดังนั้นการแทนค่าที่แม่นยำกว่า (แม้ว่าจะอ่านไม่ได้) ของวัตถุในหน่วยความจำจะเป็น:
ลิงค์ที่เป็นประโยชน์:
tuple
โครงสร้างในที่เก็บ CPython สำหรับ Python 2.7list
โครงสร้างในที่เก็บ CPython สำหรับ Python 2.7int
โครงสร้างในที่เก็บ CPython สำหรับ Python 2.7โปรดทราบ__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__
)
list
จัดสรรหน่วยความจำstackoverflow.com/questions/40018398/…
list()
หรือความเข้าใจในรายการ
ฉันจะเจาะลึกเกี่ยวกับ 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);
}
นี้เป็นสำหรับlist
S, คว้า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
ของtuple
s คำนวณได้ดังนี้:
sizeof(PyTupleObject) - sizeof(PyObject *)
ทำไม8
ไบต์เพิ่มเติมจึงถูกลบออกtp_basicsize
จึงเป็นสิ่งที่ฉันไม่สามารถหาได้ (ดูความคิดเห็นของ MSeifert สำหรับคำอธิบายที่เป็นไปได้)
แต่นี้เป็นพื้นแตกต่างในตัวอย่างของคุณโดยเฉพาะ list
นอกจากนี้ยังเก็บองค์ประกอบที่จัดสรรไว้จำนวนหนึ่งซึ่งช่วยในการกำหนดว่าเมื่อใดจะจัดสรรมากเกินไปอีกครั้ง
ตอนนี้เมื่อมีการเพิ่มองค์ประกอบเพิ่มเติมรายการจะดำเนินการจัดสรรส่วนเกินนี้เพื่อให้บรรลุ O (1) ต่อท้าย ส่งผลให้มีขนาดใหญ่ขึ้นเนื่องจาก MSeifert ครอบคลุมคำตอบของเขาเป็นอย่างดี
ob_item[1]
ส่วนใหญ่เป็นตัวยึดตำแหน่ง (ดังนั้นจึงเหมาะสมที่จะหักออกจากขนาดพื้นฐาน) มีการจัดสรรการใช้tuple
PyObject_NewVar
ฉันยังไม่ได้หารายละเอียดดังนั้นนั่นเป็นเพียงการคาดเดาที่มีการศึกษา ...
คำตอบของ MSeifert ครอบคลุมกว้าง ๆ เพื่อให้ง่ายขึ้นคุณสามารถนึกถึง:
tuple
ไม่เปลี่ยนรูป เมื่อตั้งค่าแล้วคุณจะไม่สามารถเปลี่ยนแปลงได้ คุณจึงรู้ล่วงหน้าว่าคุณต้องจัดสรรหน่วยความจำให้กับวัตถุนั้นมากแค่ไหน
list
ไม่แน่นอน คุณสามารถเพิ่มหรือลบรายการเข้าหรือออกได้ มันต้องรู้ขนาดของมัน (สำหรับการบอกต่อภายใน) มันปรับขนาดได้ตามต้องการ
ไม่มีอาหารฟรี - ความสามารถเหล่านี้มาพร้อมกับค่าใช้จ่าย ดังนั้นค่าใช้จ่ายในหน่วยความจำสำหรับรายการ
ขนาดของทูเปิลเป็นคำนำหน้าซึ่งหมายความว่าในการเริ่มต้นทูเปิลล่ามจะจัดสรรพื้นที่เพียงพอสำหรับข้อมูลที่มีอยู่และนั่นคือจุดสิ้นสุดของมันทำให้ไม่เปลี่ยนรูป (ไม่สามารถแก้ไขได้) ในขณะที่รายการเป็นวัตถุที่เปลี่ยนแปลงไม่ได้ดังนั้นจึงหมายถึงไดนามิก การจัดสรรหน่วยความจำดังนั้นเพื่อหลีกเลี่ยงการจัดสรรพื้นที่ทุกครั้งที่คุณต่อท้ายหรือแก้ไขรายการ (จัดสรรพื้นที่ให้เพียงพอที่จะมีข้อมูลที่เปลี่ยนแปลงและคัดลอกข้อมูลไป) มันจะจัดสรรพื้นที่เพิ่มเติมสำหรับการผนวกการแก้ไขในอนาคต ... สรุปได้