อะไรทำให้ [* a] แสดงผลโดยรวม


136

เห็นได้ชัดว่าlist(a)ไม่รวม overallocate, [x for x in a]overallocates ในบางจุดและ[*a]overallocates ตลอดเวลา ?

ขนาดสูงสุด n = 100

นี่คือขนาด n จาก 0 ถึง 12 และขนาดผลลัพธ์เป็นไบต์สำหรับวิธีการสามวิธี:

0 56 56 56
1 64 88 88
2 72 88 96
3 80 88 104
4 88 88 112
5 96 120 120
6 104 120 128
7 112 120 136
8 120 120 152
9 128 184 184
10 136 184 192
11 144 184 200
12 152 184 208

คำนวณแบบนี้สามารถทำซ้ำได้ที่ repl.itโดยใช้ Python 3 8 :

from sys import getsizeof

for n in range(13):
    a = [None] * n
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]))

ดังนั้นมันทำงานอย่างไร อย่างไร[*a]overallocate? จริงๆแล้วมันใช้กลไกอะไรในการสร้างรายการผลลัพธ์จากอินพุตที่กำหนด? มันใช้ตัววนซ้ำaและใช้บางอย่างเช่นlist.append? ซอร์สโค้ดอยู่ที่ไหน

( Colab ที่มีข้อมูลและรหัสที่สร้างภาพ)

ซูมเข้าสู่ n ที่เล็กกว่า:

ขนาดสูงสุด n = 40

ซูมออกไปใหญ่กว่า n:

ขนาดสูงสุด n = 1,000


1
FWIW ขยายกรณีทดสอบของคุณก็จะดูเหมือนว่าพฤติกรรมของรายการความเข้าใจการเขียนห่วงและท้ายแต่ละรายการไปยังรายการในขณะที่[*a]ปรากฏขึ้นในการทำงานเช่นเดียวกับที่ใช้extendในรายการที่ว่างเปล่า
jdehesa

4
มันอาจช่วยในการดูรหัสไบต์ที่สร้างขึ้นสำหรับแต่ละ list(a)ดำเนินการทั้งหมดใน C; มันสามารถจัดสรรโหนดบัฟเฟอร์ภายในโหนดเป็นมัน iterates aกว่า [x for x in a]ใช้LIST_APPENDจำนวนมากดังนั้นจึงเป็นไปตามรูปแบบ "overallocate เล็กน้อยปกติจัดสรรใหม่เมื่อจำเป็น" ของรายการปกติ [*a]การใช้BUILD_LIST_UNPACKงานที่ ... ฉันไม่รู้ว่าจะทำอะไรนอกเหนือจากที่เห็นได้ชัดว่าจัดสรรเกินตลอดเวลา :)
chepner

2
ยิ่งไปกว่านั้นใน Python 3.7 ดูเหมือนว่าlist(a)และ[*a]เหมือนกันและมีทั้ง overallocate เปรียบเทียบกับ[x for x in a]ดังนั้น ... sys.getsizeofอาจไม่ใช่เครื่องมือที่เหมาะสมที่จะใช้ที่นี่
chepner

7
@ chepner ฉันคิดว่าsys.getsizeofเป็นเครื่องมือที่ถูกต้องมันแค่แสดงให้เห็นว่าlist(a)เคยใช้ใน อันที่จริงมีอะไรใหม่ในหลาม 3.8กล่าวถึงมัน"ตัวสร้างรายการไม่ overallocate [ ... ]"
Stefan Pochmann

5
@chepner: นั่นเป็นข้อผิดพลาดคงที่ใน 3.8 ; คอนสตรัคไม่ควรจะรวมโดยรวม
ShadowRanger

คำตอบ:


81

[*a] กำลังทำภายในเทียบเท่ากับ C :

  1. ทำใหม่ที่ว่างเปล่า list
  2. โทร newlist.extend(a)
  3. ผลตอบแทน listผลตอบแทน

ดังนั้นหากคุณขยายการทดสอบของคุณเป็น:

from sys import getsizeof

for n in range(13):
    a = [None] * n
    l = []
    l.extend(a)
    print(n, getsizeof(list(a)),
             getsizeof([x for x in a]),
             getsizeof([*a]),
             getsizeof(l))

ลองออนไลน์!

คุณจะเห็นผลลัพธ์สำหรับgetsizeof([*a])และl = []; l.extend(a); getsizeof(l)เหมือนกัน

นี่เป็นสิ่งที่ควรทำ เมื่อextendคุณมักจะคาดหวังว่าจะเพิ่มมากขึ้นในภายหลังและในทำนองเดียวกันสำหรับการนำออกจากกล่องโดยทั่วไปจะถือว่ามีหลายสิ่งที่จะถูกเพิ่มเข้าด้วยกัน [*a]ไม่ใช่กรณีปกติ Python สมมติว่ามีหลายรายการหรือเพิ่ม iterables ลงในlist( [*a, b, c, *d]) ดังนั้นการบันทึกผลรวมจะบันทึกงานในกรณีทั่วไป

ในทางตรงกันข้ามสิ่งlistก่อสร้างที่สร้างขึ้นจากการทำซ้ำ (ที่มีlist()) ที่กำหนดไว้แล้วอาจไม่เติบโตหรือหดตัวระหว่างการใช้งาน งูใหญ่เมื่อเร็ว ๆ นี้ได้รับการแก้ไขข้อผิดพลาดที่ทำให้ overallocate สร้างแม้สำหรับปัจจัยการผลิตที่มีขนาดที่รู้จัก

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

เพื่อความชัดเจนไม่มีการรับประกันภาษาใด ๆ เป็นเพียงวิธีที่ CPython ใช้งาน ข้อมูลจำเพาะภาษา Python โดยทั่วไปไม่เกี่ยวข้องกับรูปแบบการเติบโตเฉพาะในlist(นอกเหนือจากการรับประกันO(1) appends และ s ตัดจำหน่ายpopจากจุดสิ้นสุด) ตามที่ระบุไว้ในความคิดเห็นการดำเนินการเฉพาะการเปลี่ยนแปลงอีกครั้งใน 3.9; แม้ว่ามันจะไม่ส่งผลกระทบ[*a]แต่ก็อาจส่งผลต่อกรณีอื่น ๆ ที่สิ่งที่เคยเป็น "สร้างรายการชั่วคราวtupleของแต่ละรายการและextendด้วยtuple" ตอนนี้กลายเป็นแอปพลิเคชันหลายรายการLIST_APPENDซึ่งสามารถเปลี่ยนแปลงได้เมื่อภาพรวมเกิดขึ้น


4
@StefanPochmann: ฉันเคยอ่านรหัสมาก่อน (ซึ่งเป็นสาเหตุที่ฉันรู้เรื่องนี้มาแล้ว) นี่คือตัวจัดการโค้ดไบต์สำหรับBUILD_LIST_UNPACKมันใช้_PyList_Extendเป็นเทียบเท่า C ของการโทรextend(เพียงโดยตรงมากกว่าโดยวิธีการค้นหา) พวกเขารวมมันกับเส้นทางสำหรับการสร้าง a tupleด้วยการเปิดออก tuples ไม่ overallocate อย่างดีสำหรับอาคารทีละน้อยดังนั้นพวกเขามักจะแกะเพื่อlist(ได้รับประโยชน์จาก overallocation) และแปลงtupleเป็นตอนท้ายเมื่อเป็นสิ่งที่ถูกร้องขอ
ShadowRanger

4
หมายเหตุว่านี้เห็นได้ชัดว่ามีการเปลี่ยนแปลงใน 3.9ที่การก่อสร้างจะทำกับ bytecodes แยกต่างหาก ( BUILD_LIST, LIST_EXTENDสำหรับสิ่งที่จะต้องแกะแต่ละLIST_APPENDสำหรับรายการเดียว) แทนการโหลดทุกอย่างในกองก่อนที่จะสร้างทั้งlistที่มีการเรียนการสอนรหัส byte เดียว (จะช่วยให้ คอมไพเลอร์เพื่อดำเนินการปรับให้เหมาะสมที่คำสั่งแบบ all-in-one ไม่อนุญาตเช่นการนำไปใช้[*a, b, *c]เป็นLIST_EXTEND, LIST_APPENDโดยที่ไม่LIST_EXTENDจำเป็นต้องห่อหุ้มbในหนึ่ง - tupleเพื่อตอบสนองความต้องการของBUILD_LIST_UNPACK)
ShadowRanger

18

ภาพเต็มของสิ่งที่เกิดขึ้นสร้างคำตอบและความคิดเห็นอื่น ๆ (โดยเฉพาะคำตอบของ ShadowRangerซึ่งอธิบายว่าทำไมมันถึงทำแบบนั้น)

การแยกชิ้นส่วนที่แสดงว่าBUILD_LIST_UNPACKใช้แล้ว:

>>> import dis
>>> dis.dis('[*a]')
  1           0 LOAD_NAME                0 (a)
              2 BUILD_LIST_UNPACK        1
              4 RETURN_VALUE

ที่จัดการในceval.cซึ่งสร้างรายการที่ว่างเปล่าและขยาย (ด้วยa):

        case TARGET(BUILD_LIST_UNPACK): {
            ...
            PyObject *sum = PyList_New(0);
              ...
                none_val = _PyList_Extend((PyListObject *)sum, PEEK(i));

_PyList_Extend ใช้ list_extend :

_PyList_Extend(PyListObject *self, PyObject *iterable)
{
    return list_extend(self, iterable);
}

ซึ่งเรียกlist_resizeด้วยผลรวมของขนาด :

list_extend(PyListObject *self, PyObject *iterable)
    ...
        n = PySequence_Fast_GET_SIZE(iterable);
        ...
        m = Py_SIZE(self);
        ...
        if (list_resize(self, m + n) < 0) {

และoverallocatesดังต่อไปนี้:

list_resize(PyListObject *self, Py_ssize_t newsize)
{
  ...
    new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

มาตรวจสอบกันก่อนว่า คำนวณจำนวนสปอตที่คาดหวังด้วยสูตรข้างต้นและคำนวณขนาดไบต์ที่คาดหวังโดยการคูณด้วย 8 (ขณะที่ฉันใช้ Python 64 บิตที่นี่) และเพิ่มขนาดไบต์ของรายการที่ว่างเปล่า (เช่นค่าคงที่ของรายการของวัตถุ) :

from sys import getsizeof
for n in range(13):
    a = [None] * n
    expected_spots = n + (n >> 3) + (3 if n < 9 else 6)
    expected_bytesize = getsizeof([]) + expected_spots * 8
    real_bytesize = getsizeof([*a])
    print(n,
          expected_bytesize,
          real_bytesize,
          real_bytesize == expected_bytesize)

เอาท์พุท:

0 80 56 False
1 88 88 True
2 96 96 True
3 104 104 True
4 112 112 True
5 120 120 True
6 128 128 True
7 136 136 True
8 152 152 True
9 184 184 True
10 192 192 True
11 200 200 True
12 208 208 True

ยกเว้นตรงกับn = 0ที่list_extendจริงทางลัดเพื่อให้จริงว่าตรงเกินไป:

        if (n == 0) {
            ...
            Py_RETURN_NONE;
        }
        ...
        if (list_resize(self, m + n) < 0) {

8

สิ่งเหล่านี้จะเป็นรายละเอียดการใช้งานของล่าม CPython และอาจไม่สอดคล้องกับล่ามอื่น ๆ

ที่กล่าวมาคุณจะเห็นว่าความเข้าใจและlist(a)พฤติกรรมมาที่นี่:

https://github.com/python/cpython/blob/master/Objects/listobject.c#L36

โดยเฉพาะสำหรับความเข้าใจ:

 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
...

new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);

เพียงด้านล่างเส้นที่มีซึ่งจะใช้เมื่อเรียกlist_preallocate_exactlist(a)


1
[*a]ไม่ได้ต่อท้ายองค์ประกอบทีละรายการ มันมีของมัน bytecode extendทุ่มเทตัวเองที่ไม่แทรกเป็นกลุ่มผ่านทาง
ShadowRanger

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