เหตุใดรายการที่เหมือนกันสองรายการจึงมีหน่วยความจำแตกต่างกัน


155

ฉันสร้างสองรายการl1และl2แต่แต่ละคนด้วยวิธีการสร้างที่แตกต่างกัน:

import sys

l1 = [None] * 10
l2 = [None for _ in range(10)]

print('Size of l1 =', sys.getsizeof(l1))
print('Size of l2 =', sys.getsizeof(l2))

แต่ผลลัพธ์ที่ทำให้ฉันประหลาดใจ:

Size of l1 = 144
Size of l2 = 192

รายการที่สร้างด้วย list comprehension มีขนาดใหญ่กว่าในหน่วยความจำ แต่ทั้งสองรายการนั้นเหมือนกันใน Python

ทำไมถึงเป็นอย่างนั้น? นี่เป็นสิ่งภายใน CPython หรือมีคำอธิบายอื่น ๆ


2
อาจเป็นไปได้ว่าผู้ดำเนินการทำซ้ำจะเรียกใช้ฟังก์ชันบางอย่างที่มีขนาดอาร์เรย์ที่แน่นอน โปรดทราบว่า144 == sys.getsizeof([]) + 8*10)โดยที่ 8 คือขนาดของตัวชี้
juanpa.arrivillaga

1
โปรดทราบว่าถ้าคุณเปลี่ยน10ไป11ที่[None] * 11รายการมีขนาดแต่ความเข้าใจของรายการยังคงมีขนาด152 192คำถามที่เชื่อมโยงก่อนหน้านี้ไม่ใช่คำถามที่ซ้ำกัน แต่มีความเกี่ยวข้องในการทำความเข้าใจว่าทำไมสิ่งนี้ถึงเกิดขึ้น
Patrick Haugh

คำตอบ:


162

เมื่อคุณเขียน[None] * 10Python รู้ว่ามันจะต้องมีรายการของวัตถุ 10 รายการดังนั้นมันจึงจัดสรรอย่างนั้น

เมื่อคุณใช้ list comprehension Python จะไม่รู้ว่าจำเป็นต้องใช้มากแค่ไหน ดังนั้นจึงค่อย ๆ เพิ่มรายการตามที่มีการเพิ่มองค์ประกอบ สำหรับการจัดสรรใหม่แต่ละครั้งจะจัดสรรพื้นที่มากกว่าที่จำเป็นในทันทีเพื่อให้ไม่ต้องจัดสรรใหม่สำหรับแต่ละองค์ประกอบ รายการผลลัพธ์น่าจะค่อนข้างใหญ่กว่าที่ต้องการ

คุณสามารถเห็นพฤติกรรมนี้เมื่อเปรียบเทียบรายการที่สร้างด้วยขนาดใกล้เคียงกัน:

>>> sys.getsizeof([None]*15)
184
>>> sys.getsizeof([None]*16)
192
>>> sys.getsizeof([None for _ in range(15)])
192
>>> sys.getsizeof([None for _ in range(16)])
192
>>> sys.getsizeof([None for _ in range(17)])
264

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


1
ใช่มันสมเหตุสมผลแล้ว มันน่าจะสร้างรายการได้ดีกว่า*เมื่อฉันรู้ขนาดด้านหน้า
Andrej Kesely

27
@AndrejKesely ใช้[x] * nกับไม่เปลี่ยนรูปxในรายการของคุณเท่านั้น รายการผลลัพธ์จะระงับการอ้างอิงไปยังวัตถุที่เหมือนกัน
schwobaseggl

5
@schwobaseggl ดีนั่นอาจเป็นสิ่งที่คุณต้องการ แต่ก็เป็นการดีที่จะเข้าใจ
juanpa.arrivillaga

19
@ juanpa.arrivillaga จริงมันอาจจะเป็น แต่โดยปกติแล้วมันจะไม่เป็นเช่นนั้นและโดยเฉพาะอย่างยิ่ง SO จึงเต็มไปด้วยผู้สงสัยว่าทำไมข้อมูลทั้งหมดของพวกเขาเปลี่ยนแปลงไปในเวลาเดียวกัน: D
schwobaseggl

50

ดังที่ระบุไว้ในคำถามนี้การใช้ list-comprehension list.appendภายใต้ประทุนนั้นจะเรียกเมธอด list-resize ซึ่ง overallocates

ในการสาธิตสิ่งนี้ให้กับตัวคุณเองคุณสามารถใช้ตัวdisแยกวิเคราะห์:

>>> code = compile('[x for x in iterable]', '', 'eval')
>>> import dis
>>> dis.dis(code)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10560b810, file "", line 1>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (iterable)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x10560b810, file "", line 1>:
  1           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE
>>>

แจ้งให้ทราบLIST_APPENDopcode ในการถอดชิ้นส่วนของ<listcomp>วัตถุรหัส จากเอกสาร :

LIST_APPEND (i)

โทรlist.append(TOS[-i], TOS). ใช้เพื่อสร้างรายการความเข้าใจ

ตอนนี้สำหรับการดำเนินการรายการซ้ำเรามีคำแนะนำเกี่ยวกับสิ่งที่เกิดขึ้นถ้าเราพิจารณา:

>>> import sys
>>> sys.getsizeof([])
64
>>> 8*10
80
>>> 64 + 80
144
>>> sys.getsizeof([None]*10)
144

ดังนั้นจึงดูเหมือนว่าจะสามารถที่จะตรงจัดสรรขนาด เมื่อมองไปที่ซอร์สโค้ดเราจะเห็นว่านี่คือสิ่งที่เกิดขึ้นจริง:

static PyObject *
list_repeat(PyListObject *a, Py_ssize_t n)
{
    Py_ssize_t i, j;
    Py_ssize_t size;
    PyListObject *np;
    PyObject **p, **items;
    PyObject *elem;
    if (n < 0)
        n = 0;
    if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n)
        return PyErr_NoMemory();
    size = Py_SIZE(a) * n;
    if (size == 0)
        return PyList_New(0);
    np = (PyListObject *) PyList_New(size);

คือที่นี่: size = Py_SIZE(a) * n;. ส่วนที่เหลือของฟังก์ชั่นเพียงแค่เติมอาร์เรย์


"ตามที่ระบุไว้ในคำถามนี้การใช้รายการเข้าใจ list.append ภายใต้ประทุน" .extend()ผมคิดว่ามันถูกต้องมากขึ้นที่จะบอกว่ามันใช้
สะสม

@ การคำนวณทำไมคุณถึงเชื่อเช่นนั้น
juanpa.arrivillaga

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

7
@ การคำนวณที่ไม่ถูกต้อง list.appendเป็นการดำเนินการเวลาคงที่ตัดจำหน่ายเพราะเมื่อรายการมีการปรับขนาดมันจะรวมโดยรวม ไม่ใช่ทุกการดำเนินการผนวกดังนั้นผลลัพธ์ในอาร์เรย์ที่จัดสรรใหม่ ในกรณีใด ๆ คำถามที่ฉันเชื่อมโยงเพื่อแสดงให้เห็นคุณในรหัสที่มาว่าในความเป็นจริง comprehensions รายการทำการใช้งานlist.append, ฉันจะกลับไปที่แล็ปท็อปของฉันในชั่วขณะหนึ่งและฉันสามารถแสดง bytecode ที่ถอดแยกชิ้นส่วนได้เพื่อทำความเข้าใจรายการและLIST_APPENDopcode ที่เกี่ยวข้อง
juanpa.arrivillaga

3

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

for ele in l2:
    print(sys.getsizeof(ele))

>>>>16
16
16
16
16
16
16
16
16
16

ซึ่งไม่ได้รวมขนาดของ l2 แต่ค่อนข้างน้อย

print(sys.getsizeof([None]))
72

l1และนี่คือมากขึ้นกว่าหนึ่งในสิบของขนาดของ

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


1
Noneไม่ได้ถูกจัดเก็บจริงในอาเรย์พื้นฐานสิ่งเดียวที่เก็บไว้คือPyObjectตัวชี้ (8 ไบต์) วัตถุ Python ทั้งหมดได้รับการจัดสรรบนฮีป Noneเป็น singleton ดังนั้นการมี list ที่มี nones จำนวนมากจะสร้างอาร์เรย์ของตัวชี้ PyObject ไปยังNoneวัตถุเดียวกันบน heap (และไม่ใช้หน่วยความจำเพิ่มเติมในกระบวนการต่อเพิ่มเติมNone) ฉันไม่แน่ใจว่าสิ่งที่คุณหมายถึงโดย "ไม่มีไม่มีขนาดที่ระบุไว้ล่วงหน้า" แต่นั่นไม่ได้เสียงที่ถูกต้อง ในที่สุดการวนซ้ำของคุณกับgetsizeofแต่ละองค์ประกอบจะไม่แสดงให้เห็นว่าคุณคิดว่ามันกำลังแสดงอะไร
juanpa.arrivillaga

ถ้าอย่างที่คุณพูดเป็นจริงขนาดของ [ไม่มี] * 10 ควรเท่ากับขนาดของ [ไม่มี] แต่ชัดเจนว่านี่ไม่ใช่ - มีการเพิ่มที่เก็บข้อมูลบางส่วน ในความเป็นจริงขนาดของ [ไม่มี] ซ้ำสิบครั้ง (160) ก็น้อยกว่าขนาดของ [ไม่มี] คูณด้วยสิบ เมื่อคุณชี้ให้เห็นอย่างชัดเจนขนาดของตัวชี้ไปที่ [ไม่มี] นั้นเล็กกว่าขนาดของ [ไม่มี] ตัวเอง (16 ไบต์มากกว่า 72 ไบต์) อย่างไรก็ตาม 160 + 32 คือ 192 ฉันไม่คิดว่าคำตอบก่อนหน้านี้แก้ปัญหาทั้งหมดได้ เป็นที่ชัดเจนว่ามีการจัดสรรหน่วยความจำขนาดเล็กจำนวนเล็กน้อย (อาจขึ้นอยู่กับสถานะของเครื่อง)
StevenJD

"ถ้าอย่างที่คุณพูดเป็นจริงขนาดของ [ไม่มี] * 10 ควรเท่ากับขนาดของ [ไม่มี]" สิ่งที่ฉันพูดที่อาจเป็นไปได้ว่า? อีกครั้งคุณดูเหมือนจะมุ่งเน้นไปที่ความจริงที่ว่าบัฟเฟอร์พื้นฐานมีการจัดสรรมากเกินไปหรือขนาดของรายการรวมมากกว่าขนาดของบัฟเฟอร์พื้นฐาน (แน่นอนว่ามันทำ) แต่นั่นไม่ใช่จุดของ คำถามนี้. อีกครั้งที่ใช้งานของคุณgestsizeofในแต่ละeleแห่งl2จะทำให้เข้าใจผิดเพราะไม่ได้คำนึงถึงขนาดขององค์ประกอบภายในภาชนะgetsizeof(l2)
juanpa.arrivillaga

เพื่อพิสูจน์ตัวเองว่าการเรียกร้องที่ผ่านมาทำแล้วl1 = [None]; l2 = [None]*100; l3 = [l2] คุณจะได้รับผลเช่น:print(sys.getsizeof(l1), sys.getsizeof(l2), sys.getsizeof(l3)) 72 864 72นั่นคือตามลำดับ64 + 1*8, 64 + 100*8และ64 + 1*8อีกสมมติว่าระบบ 64 บิตกับ 8 ไบต์ขนาดตัวชี้
juanpa.arrivillaga

1
ดังที่ฉันได้กล่าวไว้sys.getsizeof* ไม่ได้คำนึงถึงขนาดของรายการในภาชนะ จากเอกสาร : "มีเพียงการใช้หน่วยความจำที่ประกอบโดยตรงกับวัตถุเท่านั้นไม่ใช่การใช้หน่วยความจำของวัตถุที่อ้างถึง ... ดูสูตรขนาดซ้ำสำหรับตัวอย่างของการใช้ getsizeof () ซ้ำเพื่อค้นหาขนาดของคอนเทนเนอร์และ เนื้อหาทั้งหมด "
juanpa.arrivillaga
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.