Tại sao [] nhanh hơn danh sách ()?


706

Gần đây tôi đã so sánh tốc độ xử lý của []list()và đã rất ngạc nhiên phát hiện ra rằng []chạy hơn ba lần nhanh hơn list(). Tôi đã thực hiện cùng một bài kiểm tra với {}dict()kết quả thực tế giống hệt nhau: []{}cả hai đều mất khoảng 0,125 giây / triệu chu kỳ, trong khi list()dict()mất khoảng 0,428 giây / triệu chu kỳ mỗi chu kỳ.

Tại sao lại thế này? Làm []{}(và có lẽ ()'', quá) ngay lập tức vượt qua trở lại một bản sao của một số cổ phiếu đen trống rỗng trong khi các đối tác một cách rõ ràng tên của họ ( list(), dict(), tuple(), str()) đầy đủ đi về việc tạo ra một đối tượng, dù có hoặc không thực sự có yếu tố?

Tôi không biết hai phương pháp này khác nhau như thế nào nhưng tôi rất muốn tìm hiểu. Tôi không thể tìm thấy câu trả lời trong các tài liệu hoặc trên SO và việc tìm kiếm các dấu ngoặc rỗng hóa ra có nhiều vấn đề hơn tôi mong đợi.

Tôi đã nhận được kết quả thời gian của mình bằng cách gọi timeit.timeit("[]")timeit.timeit("list()"), timeit.timeit("{}")timeit.timeit("dict()"), để so sánh các danh sách và từ điển, tương ứng. Tôi đang chạy Python 2.7.9.

Gần đây tôi phát hiện ra " Tại sao là nếu Đúng chậm hơn so với nếu 1? " Mà so sánh hiệu suất của if Trueđể if 1và dường như chạm vào một tương tự đen-versus-toàn cầu kịch bản; có lẽ nó cũng đáng để xem xét


2
Lưu ý: ()''thật đặc biệt, vì chúng không chỉ trống rỗng, chúng còn bất biến, và như vậy, đó là một chiến thắng dễ dàng để biến chúng thành những người độc thân; họ thậm chí không xây dựng các đối tượng mới, chỉ tải các đơn vị trống tuple/ str. Về mặt kỹ thuật là một chi tiết triển khai, nhưng tôi có một thời gian khó tưởng tượng tại sao họ sẽ không lưu trữ trống tuple/ strvì lý do hiệu suất. Vì vậy, trực giác của bạn về []{}trả lại một nghĩa đen chứng khoán là sai, nhưng nó áp dụng cho ()''.
ShadowRanger

Câu trả lời:


757

Bởi vì []{}cú pháp theo nghĩa đen . Python có thể tạo mã byte chỉ để tạo danh sách hoặc các đối tượng từ điển:

>>> 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()là những đối tượng riêng biệt. Tên của chúng cần được giải quyết, ngăn xếp phải được tham gia để đẩy các đối số, khung phải được lưu trữ để truy xuất sau đó và phải thực hiện một cuộc gọi. Đó là tất cả mất nhiều thời gian hơn.

Đối với trường hợp trống, điều đó có nghĩa là bạn có ít nhất một LOAD_NAME(phải tìm kiếm trong không gian tên toàn cầu cũng như __builtin__mô-đun ) theo sau là a CALL_FUNCTION, để duy trì khung hiện tại:

>>> 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        

Bạn có thể thời gian tra cứu tên riêng với timeit:

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

Sự khác biệt về thời gian có lẽ là một sự va chạm băm từ điển. Trừ đi những lần đó từ những lần gọi các đối tượng đó và so sánh kết quả với thời gian để sử dụng nghĩa đen:

>>> 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

Vì vậy, phải gọi đối tượng mất thêm một 1.00 - 0.31 - 0.30 == 0.39giây cho mỗi 10 triệu cuộc gọi.

Bạn có thể tránh chi phí tra cứu toàn cầu bằng cách đặt bí danh tên toàn cầu là địa phương (sử dụng timeitthiết lập, mọi thứ bạn liên kết với tên đều là cục bộ):

>>> 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

nhưng bạn không bao giờ có thể vượt qua CALL_FUNCTIONchi phí đó .


150

list()yêu cầu tra cứu toàn cầu và gọi hàm nhưng []biên dịch thành một lệnh đơn. Xem:

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

Bởi vì listlà một hàm để chuyển đổi nói một chuỗi thành một đối tượng danh sách, trong khi []được sử dụng để tạo một danh sách ngoài bat. Hãy thử điều này (có thể có ý nghĩa hơn với bạn):

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

Trong khi

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

Cung cấp cho bạn một danh sách thực tế có chứa bất cứ thứ gì bạn đặt trong đó.


7
Điều này không trực tiếp giải quyết câu hỏi. Câu hỏi là tại sao []nhanh hơn list(), không phải tại sao ['wham bam']nhanh hơn list('wham bam').
Jeremy Visser

2
@JeremyVisser Điều đó có ý nghĩa rất nhỏ đối với tôi bởi vì []/ list()hoàn toàn giống với ['wham']/ list('wham')bởi vì chúng có cùng sự khác biệt về biến 1000/10giống như 100/1trong toán học. Về lý thuyết bạn có thể lấy đi wham bamvà thực tế vẫn sẽ như vậy, đó là list()cố gắng chuyển đổi một cái gì đó bằng cách gọi một tên hàm trong khi []sẽ chuyển thẳng chỉ là biến đổi. Các cuộc gọi chức năng là khác nhau, đây chỉ là một tổng quan logic về vấn đề, ví dụ như bản đồ mạng của một công ty cũng logic của một giải pháp / vấn đề. Bình chọn theo cách bạn muốn.
Torxed

@JeremyVisser thì ngược lại, nó cho thấy họ thực hiện các thao tác khác nhau về nội dung.
Baldrickk

20

Các câu trả lời ở đây là rất tốt, đến mức và hoàn toàn bao gồm câu hỏi này. Tôi sẽ giảm thêm một bước nữa từ mã byte cho những ai quan tâm. Tôi đang sử dụng repo gần đây nhất của CPython; các phiên bản cũ hoạt động tương tự về vấn đề này nhưng có thể có những thay đổi nhỏ.

Đây là một phân tích thực thi cho từng thứ, BUILD_LISTcho []CALL_FUNCTIONcho list().


Các BUILD_LIST hướng dẫn:

Bạn chỉ nên xem kinh dị:

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();

Tôi cực kỳ bối rối, tôi biết. Đây là cách đơn giản:

  • Tạo một danh sách mới với PyList_New(điều này chủ yếu phân bổ bộ nhớ cho một đối tượng danh sách mới),oparg báo hiệu số lượng đối số trên ngăn xếp. Thẳng đến điểm.
  • Kiểm tra rằng không có gì sai với if (list==NULL).
  • Thêm bất kỳ đối số nào (trong trường hợp của chúng tôi, điều này không được thực thi) nằm trên ngăn xếp với PyList_SET_ITEM(macro).

Không có gì ngạc nhiên khi nó nhanh! Nó được tùy chỉnh để tạo danh sách mới, không có gì khác :-)

Các CALL_FUNCTION hướng dẫn:

Đây là điều đầu tiên bạn nhìn thấy khi xem lén cách xử lý mã 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();

Trông khá vô hại đúng không? Chà, không, thật không may, không phải call_functionlà một người đơn giản sẽ gọi hàm ngay lập tức, điều đó không thể. Thay vào đó, nó lấy đối tượng từ ngăn xếp, lấy tất cả các đối số của ngăn xếp và sau đó chuyển đổi dựa trên loại đối tượng; có phải là:

Chúng tôi đang gọi listloại, đối số được truyền vào call_functionPyList_Type. CPython bây giờ phải gọi một hàm chung để xử lý bất kỳ đối tượng có thể gọi nào được đặt tên _PyObject_FastCallKeywords, yay nhiều lệnh gọi hàm hơn.

Hàm này một lần nữa thực hiện một số kiểm tra cho một số loại chức năng nhất định (mà tôi không thể hiểu tại sao) và sau đó, sau khi tạo một lệnh cho kwargs nếu được yêu cầu , tiếp tục gọi _PyObject_FastCallDict.

_PyObject_FastCallDictcuối cùng cũng đưa chúng ta đến một nơi nào đó! Sau khi thực hiện nhiều kiểm tra hơn nữa,lấy chỗ tp_calltrống từ cáitypetypechúng tôi đã chuyển qua, nghĩa là, nó lấy type.tp_call. Sau đó, nó tiến hành để tạo ra một tuple trong số các đối số được truyền vào _PyStack_AsTuplevà cuối cùng, một cuộc gọi cuối cùng có thể được thực hiện !

tp_call, phù hợp với type.__call__tiếp quản và cuối cùng tạo ra đối tượng danh sách. Nó gọi các danh sách __new__tương ứng PyType_GenericNewvà phân bổ bộ nhớ cho nó PyType_GenericAlloc: Cuối cùng, đây thực sự là phần mà nó bắt kịpPyList_New . Tất cả trước đây là cần thiết để xử lý các đối tượng theo một cách chung chung.

Cuối cùng, type_callcác cuộc gọilist.__init__ và khởi tạo danh sách với bất kỳ đối số có sẵn nào, sau đó chúng tôi sẽ quay trở lại con đường chúng tôi đã đến. :-)

Cuối cùng, hãy nhớ lại LOAD_NAME, đó là một người khác đóng góp ở đây.


Thật dễ dàng để thấy rằng, khi xử lý dữ liệu đầu vào của chúng tôi, Python thường phải nhảy qua các vòng để thực sự tìm ra Cchức năng phù hợp để thực hiện công việc. Nó không có tính tò mò khi gọi nó ngay lập tức bởi vì nó năng động, ai đó có thể che dấu list( và cậu bé làm nhiều người làm ) và một con đường khác phải được thực hiện.

Đây là nơi list()mất nhiều: Python khám phá cần phải làm gì để tìm ra cái quái gì nó nên làm.

Cú pháp nghĩa đen, mặt khác, có nghĩa là chính xác một điều; nó không thể được thay đổi và luôn luôn hành xử theo cách được xác định trước.

Chú thích: Tất cả các tên hàm có thể thay đổi từ bản phát hành này sang bản phát hành khác. Điểm vẫn còn và rất có thể sẽ đứng ở bất kỳ phiên bản nào trong tương lai, đó là vẻ ngoài năng động làm mọi thứ chậm lại.


13

Tại sao []nhanh hơn list()?

Lý do lớn nhất là Python xử lý list()giống như một hàm do người dùng định nghĩa, có nghĩa là bạn có thể chặn nó bằng cách đặt bí danh cho thứ kháclist và làm một cái gì đó khác (như sử dụng danh sách phân lớp của riêng bạn hoặc có lẽ là một deque).

Nó ngay lập tức tạo ra một phiên bản mới của danh sách dựng sẵn với [].

Giải thích của tôi tìm cách cung cấp cho bạn trực giác cho việc này.

Giải trình

[] thường được gọi là cú pháp theo nghĩa đen.

Trong ngữ pháp, điều này được gọi là "hiển thị danh sách". Từ các tài liệu :

Hiển thị danh sách là một chuỗi các biểu thức có thể trống được đặt trong dấu ngoặc vuông:

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

Hiển thị danh sách mang lại một đối tượng danh sách mới, nội dung được chỉ định bởi danh sách biểu thức hoặc mức độ hiểu. Khi một danh sách các biểu thức được phân tách bằng dấu phẩy được cung cấp, các phần tử của nó được ước tính từ trái sang phải và được đặt vào đối tượng danh sách theo thứ tự đó. Khi một sự hiểu biết được cung cấp, danh sách được xây dựng từ các yếu tố kết quả từ sự hiểu biết.

Nói tóm lại, điều này có nghĩa là một đối tượng dựng sẵn của kiểu listđược tạo.

Không có cách nào phá vỡ điều này - điều đó có nghĩa là Python có thể làm điều đó nhanh nhất có thể.

Mặt khác, list()có thể bị chặn khỏi việc tạo nội dung listbằng cách sử dụng hàm tạo danh sách dựng sẵn.

Ví dụ: giả sử chúng tôi muốn danh sách của mình được tạo ra một cách ồn ào:

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

Sau đó, chúng tôi có thể chặn tên listtrên phạm vi toàn cầu ở cấp mô-đun và sau đó khi chúng tôi tạo một list, chúng tôi thực sự tạo danh sách phụ của chúng tôi:

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

Tương tự như vậy, chúng ta có thể loại bỏ nó khỏi không gian tên toàn cầu

del list

và đặt nó trong không gian tên dựng sẵn:

import builtins
builtins.list = List

Và bây giờ:

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

Và lưu ý rằng hiển thị danh sách tạo ra một danh sách vô điều kiện:

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

Chúng tôi có lẽ chỉ làm điều này tạm thời, vì vậy hãy hoàn tác các thay đổi của chúng tôi - trước tiên hãy xóa Listđối tượng mới khỏi các nội trang:

>>> 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

Ồ, không, chúng tôi mất dấu vết của bản gốc.

Đừng lo lắng, chúng ta vẫn có thể nhận được list- đó là loại danh sách theo nghĩa đen:

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

Vì thế...

Tại sao []nhanh hơn list()?

Như chúng ta đã thấy - chúng ta có thể ghi đè lên list- nhưng chúng ta không thể chặn việc tạo ra kiểu chữ. Khi chúng tôi sử dụng, listchúng tôi phải thực hiện tra cứu để xem có gì ở đó không.

Sau đó, chúng tôi phải gọi bất cứ điều gì có thể gọi chúng tôi đã tìm kiếm. Từ ngữ pháp:

Một cuộc gọi gọi một đối tượng có thể gọi được (ví dụ: một hàm) với một loạt các đối số có thể trống:

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

Chúng ta có thể thấy rằng nó làm điều tương tự cho bất kỳ tên nào, không chỉ danh sách:

>>> 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

[]không có chức năng gọi ở cấp độ mã byte Python:

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

Nó chỉ đơn giản là đi thẳng vào việc xây dựng danh sách mà không có bất kỳ tra cứu hay cuộc gọi nào ở cấp độ mã byte.

Phần kết luận

Chúng tôi đã chứng minh rằng listcó thể chặn mã người dùng bằng cách sử dụng các quy tắc phạm vi và list()tìm kiếm một cuộc gọi và sau đó gọi nó.

Trong khi đó []là một hiển thị danh sách, hoặc một nghĩa đen, và do đó tránh việc tra cứu tên và gọi hàm.


2
+1 để chỉ ra rằng bạn có thể chiếm quyền điều khiển listvà trình biên dịch python không thể chắc chắn liệu nó có thực sự trả về một danh sách trống hay không.
Beefster
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.