เหตุใด [] จึงเร็วกว่ารายการ ()


707

ฉันเพิ่งเทียบความเร็วการประมวลผลของ[]และlist()และรู้สึกประหลาดใจที่พบว่า[]วิ่งเกินสามครั้งเร็วlist()กว่า ฉันทำการทดสอบเดียวกันด้วย{}และdict()ผลลัพธ์ก็เหมือนกันจริง: []และ{}ทั้งคู่ใช้เวลาประมาณ 0.128 วินาที / ล้านรอบในขณะที่list()และdict()ใช้เวลาประมาณ 0.428 วินาที / ล้านรอบ

ทำไมนี้ ทำ[]และ{}(และอาจจะ()และ''ด้วย) ทันทีส่งกลับสำเนาของบางตัวอักษรหุ้นที่ว่างเปล่าในขณะที่ลูกน้องอย่างชัดเจนชื่อของพวกเขา ( list(), dict(), tuple(), str()) อย่างเต็มที่ไปเกี่ยวกับการสร้างวัตถุหรือไม่ว่าพวกเขาเป็นจริงมีองค์ประกอบ?

ฉันไม่รู้ว่าทั้งสองวิธีต่างกันอย่างไร แต่ฉันชอบที่จะรู้ ฉันไม่พบคำตอบในเอกสารหรือบน SO และการค้นหาวงเล็บว่างเปล่ากลายเป็นปัญหามากกว่าที่ฉันคาดไว้

ฉันได้ผลลัพธ์ตามกำหนดเวลาโดยการโทรtimeit.timeit("[]")และtimeit.timeit("list()")และtimeit.timeit("{}")และtimeit.timeit("dict()")เพื่อเปรียบเทียบรายการและพจนานุกรมตามลำดับ ฉันใช้ Python 2.7.9

เมื่อเร็ว ๆ นี้ฉันค้นพบ " ทำไมถ้า True ช้ากว่าถ้า 1? " ที่เปรียบเทียบประสิทธิภาพของif Trueการif 1และดูเหมือนว่าจะได้สัมผัสกับสถานการณ์ตามตัวอักษรและทั่วโลกที่คล้ายกัน บางทีมันก็คุ้มค่าที่จะพิจารณาเช่นกัน


2
หมายเหตุ: ()และ''มีความพิเศษเนื่องจากไม่เพียง แต่ว่างเปล่า แต่ยังไม่เปลี่ยนรูปและเช่นนี้จึงเป็นชัยชนะที่ง่ายที่จะทำให้พวกเขาเป็นโสด พวกเขาไม่ได้สร้างวัตถุใหม่เพียงแค่โหลดเดี่ยวสำหรับที่ว่างเปล่า/tuple strรายละเอียดการใช้งานทางเทคนิค แต่ฉันมีเวลาจินตนาการว่าทำไมพวกเขาจะไม่ แคชเปล่าtuple/ strด้วยเหตุผลด้านประสิทธิภาพ ดังนั้นสัญชาตญาณของคุณเกี่ยวกับ[]และ{}ผ่านการกลับตัวอักษรหุ้นเป็นเรื่องที่ผิด แต่ก็ไม่นำไปใช้และ() ''
ShadowRanger

คำตอบ:


758

เพราะ[]และ{}มีไวยากรณ์ที่แท้จริง Python สามารถสร้าง bytecode เพื่อสร้างรายการหรือวัตถุพจนานุกรม:

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

list()และdict()เป็นวัตถุแยกต่างหาก ชื่อของพวกเขาจะต้องได้รับการแก้ไขสแต็กจะต้องเกี่ยวข้องกับการผลักดันข้อโต้แย้งกรอบจะต้องมีการจัดเก็บเพื่อดึงในภายหลังและจะต้องมีการโทร ทั้งหมดนั้นใช้เวลามากกว่า

สำหรับกรณีที่ว่างเปล่านั่นหมายความว่าคุณมีอย่างน้อย a LOAD_NAME(ซึ่งต้องค้นหาผ่าน namespace ทั่วโลกรวมถึง__builtin__โมดูล ) ตามด้วย a CALL_FUNCTIONซึ่งต้องรักษาเฟรมปัจจุบัน:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

คุณสามารถกำหนดเวลาการค้นหาชื่อแยกจากกันด้วยtimeit:

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

ความคลาดเคลื่อนของเวลาที่อาจเกิดจากการแฮชของพจนานุกรม ลบเวลาเหล่านั้นออกจากเวลาเพื่อเรียกวัตถุเหล่านั้นและเปรียบเทียบผลลัพธ์กับเวลาในการใช้ตัวอักษร:

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

ดังนั้นการเรียกวัตถุจึงใช้เวลาเพิ่มอีก1.00 - 0.31 - 0.30 == 0.3910 วินาทีต่อการโทร 10 ล้านครั้ง

คุณสามารถหลีกเลี่ยงค่าใช้จ่ายการค้นหาทั่วโลกได้โดยการใช้ชื่อแทนเป็นชื่อท้องถิ่น (โดยใช้การtimeitตั้งค่าทุกสิ่งที่คุณผูกไว้กับชื่อนั้นเป็นชื่อท้องถิ่น):

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

แต่คุณไม่สามารถเอาชนะCALL_FUNCTIONต้นทุนนั้นได้


150

list()ต้องมีการค้นหาทั่วโลกและการเรียกใช้ฟังก์ชัน แต่[]รวบรวมให้เป็นคำสั่งเดียว ดู:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None

75

เพราะlistเป็นฟังก์ชั่นในการแปลงพูดสตริงเพื่อวัตถุรายการในขณะที่[]ใช้ในการสร้างรายการออกค้างคาว ลองใช้วิธีนี้ (อาจเหมาะสมกับคุณมากขึ้น):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

ในขณะที่

y = ["wham bam"]
>>> y
["wham bam"]

ให้รายการจริงที่มีสิ่งที่คุณใส่ไว้


7
สิ่งนี้ไม่ได้ตอบคำถามโดยตรง คำถามคือว่าทำไมถึง[]จะเร็วกว่าlist()ไม่ว่าทำไมจะเร็วกว่า['wham bam'] list('wham bam')
Jeremy Visser

2
@JeremyVisser ที่ทำให้ฉันรู้สึกเล็กน้อยเพราะ[]/ list()เหมือนกับ['wham']/ list('wham')เพราะพวกเขามีความแตกต่างของตัวแปร1000/10เดียวกันเช่นเดียวกับ100/1ในคณิตศาสตร์ ในทางทฤษฎีคุณสามารถนำออกไปwham bamและความจริงก็ยังคงเหมือนเดิมนั่นคือlist()พยายามแปลงบางสิ่งโดยเรียกชื่อฟังก์ชันในขณะที่[]จะแปลงค่าตัวแปรให้ตรง การเรียกใช้ฟังก์ชันแตกต่างกันใช่นี่เป็นเพียงภาพรวมเชิงตรรกะของปัญหาเช่นแผนที่เครือข่ายของ บริษัท ก็มีเหตุผลของการแก้ปัญหา / โหวตอย่างไรก็ตามคุณต้องการ
Torxed

@JeremyVisser ในทางตรงกันข้ามมันแสดงให้เห็นว่าพวกเขาดำเนินการต่าง ๆ ในเนื้อหา
Baldrickk

20

คำตอบที่นี่ยอดเยี่ยมตรงประเด็นและครอบคลุมคำถามนี้อย่างเต็มที่ ฉันจะวางขั้นตอนต่อไปจากรหัสไบต์สำหรับผู้ที่สนใจ ฉันใช้ repo ล่าสุดของ CPython; รุ่นที่เก่ากว่ามีพฤติกรรมคล้ายกันในเรื่องนี้ แต่อาจมีการเปลี่ยนแปลงเล็กน้อย

นี่คือการลงแบ่งของการดำเนินการของแต่ละเหล่านี้BUILD_LISTสำหรับ[]และสำหรับCALL_FUNCTIONlist()


การBUILD_LISTเรียนการสอน:

คุณควรจะดูหนังสยองขวัญ:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();

ฉันรู้ว่ามันซับซ้อนมาก นี่คือความเรียบง่าย:

  • สร้างรายการใหม่ด้วยPyList_New(ส่วนใหญ่จะจัดสรรหน่วยความจำสำหรับวัตถุรายการใหม่) opargส่งสัญญาณจำนวนอาร์กิวเมนต์บนสแต็ก ตรงไปยังจุดที่
  • if (list==NULL)ตรวจสอบว่าไม่มีอะไรผิดพลาดกับ
  • เพิ่มอาร์กิวเมนต์ใด ๆ (ในกรณีนี้เราไม่ได้ดำเนินการ) ซึ่งอยู่ในสแต็กด้วยPyList_SET_ITEM(แมโคร)

ไม่แปลกใจเลยที่มันเร็ว! มันทำขึ้นเองสำหรับการสร้างรายการใหม่ไม่มีอะไรอื่น :-)

การCALL_FUNCTIONเรียนการสอน:

นี่คือสิ่งแรกที่คุณเห็นเมื่อคุณดูที่การจัดการโค้ดCALL_FUNCTION:

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

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

  • PyCFunction_Type? Nope มันเป็นlist, listไม่ได้เป็นประเภทPyCFunction
  • PyMethodType? ไม่เห็นก่อนหน้านี้
  • PyFunctionType? Nopee ดูก่อนหน้า

เรากำลังเรียกlistชนิดอาร์กิวเมนต์ผ่านในการมีcall_function PyList_TypeCPython ตอนนี้จะต้องเรียกใช้ฟังก์ชั่นทั่วไปในการจัดการกับวัตถุที่เรียกได้ว่าชื่อ_PyObject_FastCallKeywordsเรียกฟังก์ชั่นมากขึ้น

ฟังก์ชั่นนี้อีกครั้งทำให้การตรวจสอบบางประเภทฟังก์ชั่นบางอย่าง (ซึ่งผมไม่เข้าใจว่าทำไม) แล้วหลังจากที่สร้าง Dict สำหรับ kwargs ถ้าจำเป็นต้องใช้_PyObject_FastCallDictไปในการเรียกร้อง

_PyObject_FastCallDictในที่สุดก็พาเราไปที่ไหนสักแห่ง! หลังจากการดำเนินการตรวจสอบมากยิ่งขึ้น ก็คว้าtp_callสล็อตจากtypeของtypeเราได้ผ่าน, ที่อยู่, type.tp_callคว้ามัน จากนั้นจะดำเนินการสร้าง tuple จากอาร์กิวเมนต์ที่ส่งผ่านด้วย_PyStack_AsTupleและในที่สุดสามารถทำการโทรได้ !

tp_callซึ่งการจับคู่type.__call__จะใช้เวลามากกว่าและในที่สุดก็สร้างรายการวัตถุ มันเรียกร้องรายการ__new__ซึ่งสอดคล้องกับPyType_GenericNewหน่วยความจำและจัดสรรให้มันกับPyType_GenericAlloc: นี้เป็นจริงส่วนหนึ่งที่จะจับขึ้นมาด้วยPyList_Newในที่สุด ก่อนหน้าทั้งหมดมีความจำเป็นในการจัดการวัตถุในแบบทั่วไป

ในท้ายที่สุดการtype_callโทรlist.__init__และเริ่มต้นรายการด้วยอาร์กิวเมนต์ใด ๆ ที่มีอยู่จากนั้นเราจะกลับไปตามวิธีที่เรามา :-)

ในที่สุดจำได้LOAD_NAMEว่าเป็นผู้ชายอีกคนที่มีส่วนร่วมที่นี่


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

นี่คือที่ที่list()สูญเสียมาก: การสำรวจ Python จำเป็นต้องทำเพื่อค้นหาสิ่งที่ heck มันควรทำ

ในทางตรงกันข้ามไวยากรณ์แท้จริงแล้วมีความหมายเดียว ไม่สามารถเปลี่ยนแปลงได้และจะทำงานในลักษณะที่กำหนดไว้ล่วงหน้า

เชิงอรรถ: ชื่อฟังก์ชั่นทั้งหมดอาจมีการเปลี่ยนแปลงจากรุ่นหนึ่งไปยังอีก จุดยังคงอยู่และส่วนใหญ่จะอยู่ในรุ่นอนาคตใด ๆ มันเป็นแบบไดนามิกการค้นหาที่ช้าลง


13

ทำไมจึง[]เร็วกว่าlist()?

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

มันจะสร้างตัวอย่างใหม่ของรายการ builtin []ทันที

คำอธิบายของฉันพยายามที่จะให้คุณปรีชาสำหรับเรื่องนี้

คำอธิบาย

[] เป็นที่รู้จักกันทั่วไปว่าเป็นตัวอักษรไวยากรณ์

ในไวยากรณ์นี้เรียกว่า "การแสดงรายการ" จากเอกสาร :

การแสดงรายการเป็นชุดของนิพจน์ที่ว่างเปล่าที่อยู่ในวงเล็บเหลี่ยม:

list_display ::=  "[" [starred_list | comprehension] "]"

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

ในระยะสั้นซึ่งหมายความว่ามีการสร้างวัตถุชนิดlistภายใน

ไม่มีการหลีกเลี่ยงสิ่งนี้ - ซึ่งหมายความว่า Python สามารถทำได้โดยเร็วที่สุด

ในทางกลับกันlist()สามารถถูกดักจับจากการสร้าง builtin listโดยใช้ builtin list constructor

ตัวอย่างเช่นสมมติว่าเราต้องการให้รายการของเราถูกสร้างขึ้นอย่างดัง

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

จากนั้นเราสามารถตัดชื่อlistในขอบเขตโกลบอลระดับโมดูลและจากนั้นเมื่อเราสร้างlistเราจะสร้างรายการย่อยของเรา:

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

ในทำนองเดียวกันเราสามารถลบออกจาก namespace ทั่วโลก

del list

และวางไว้ในเนมสเปซ builtin:

import builtins
builtins.list = List

และตอนนี้:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

และโปรดทราบว่าการแสดงรายการสร้างรายการโดยไม่มีเงื่อนไข:

>>> list_1 = []
>>> type(list_1)
<class 'list'>

เราอาจทำสิ่งนี้เป็นการชั่วคราวเท่านั้นดังนั้นให้ยกเลิกการเปลี่ยนแปลงของเราก่อน - ลบListวัตถุใหม่ออกจาก builtins:

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined

โอ้ไม่พวกเราหลงทางต้นฉบับ

ไม่ต้องกังวลเรายังสามารถรับได้list- เป็นประเภทของรายการตามตัวอักษร:

>>> builtins.list = type([])
>>> list()
[]

ดังนั้น...

ทำไมจึง[]เร็วกว่าlist()?

อย่างที่เราเห็น - เราสามารถเขียนทับlist- แต่เราไม่สามารถสกัดกั้นการสร้างประเภทตามตัวอักษร เมื่อเราใช้listเราจะต้องทำการค้นหาเพื่อดูว่ามีอะไรบ้าง

จากนั้นเราต้องโทรหาอะไรก็ได้ที่เรามองหา จากไวยากรณ์:

การโทรเรียกวัตถุที่เรียกได้ (เช่นฟังก์ชั่น) ที่มีชุดของอาร์กิวเมนต์ที่ว่างเปล่า:

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"

เราจะเห็นว่ามันทำสิ่งเดียวกันสำหรับชื่อใด ๆ ไม่ใช่แค่รายการ:

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

สำหรับ[]ไม่มีการเรียกใช้ฟังก์ชันในระดับ bytecode หลาม:

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

เพียงแค่ไปที่การสร้างรายการโดยไม่มีการค้นหาหรือการโทรที่ระดับ bytecode

ข้อสรุป

เราได้แสดงให้เห็นว่าlistสามารถถูกสกัดกั้นด้วยรหัสผู้ใช้โดยใช้กฎการกำหนดขอบเขตและlist()มองหา callable และเรียกมันว่า

ในขณะที่[]การแสดงรายการหรือตัวอักษรจึงหลีกเลี่ยงการค้นหาชื่อและการเรียกใช้ฟังก์ชัน


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