Tại sao sớm trở lại chậm hơn so với khác?


179

Đây là một câu hỏi tiếp theo cho một câu trả lời tôi đã đưa ra một vài ngày trước . Chỉnh sửa: có vẻ như OP của câu hỏi đó đã sử dụng mã tôi đã đăng cho anh ta để hỏi cùng một câu hỏi , nhưng tôi không biết về nó. Lời xin lỗi. Các câu trả lời được cung cấp là khác nhau mặc dù!

Thực chất tôi đã quan sát thấy rằng:

>>> def without_else(param=False):
...     if param:
...         return 1
...     return 0
>>> def with_else(param=False):
...     if param:
...         return 1
...     else:
...         return 0
>>> from timeit import Timer as T
>>> T(lambda : without_else()).repeat()
[0.3011460304260254, 0.2866089344024658, 0.2871549129486084]
>>> T(lambda : with_else()).repeat()
[0.27536892890930176, 0.2693932056427002, 0.27011704444885254]
>>> T(lambda : without_else(True)).repeat()
[0.3383951187133789, 0.32756996154785156, 0.3279120922088623]
>>> T(lambda : with_else(True)).repeat()
[0.3305950164794922, 0.32186388969421387, 0.3209099769592285]

... Hoặc nói cách khác: có elsemệnh đề nhanh hơn bất kể ifđiều kiện được kích hoạt hay không.

Tôi giả sử nó phải làm với mã byte khác nhau do hai người tạo ra, nhưng có ai có thể xác nhận / giải thích chi tiết không?

EDIT: Có vẻ như không phải ai cũng có thể sao chép thời gian của tôi, vì vậy tôi nghĩ rằng có thể hữu ích khi cung cấp một số thông tin trên hệ thống của tôi. Tôi đang chạy Ubuntu 11.10 64 bit với cài đặt python mặc định. pythontạo thông tin phiên bản sau:

Python 2.7.2+ (default, Oct  4 2011, 20:06:09) 
[GCC 4.6.1] on linux2

Dưới đây là kết quả của quá trình tháo gỡ trong Python 2.7:

>>> dis.dis(without_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  4     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
>>> dis.dis(with_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  5     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        

1
có một câu hỏi giống hệt nhau trên SO tôi không thể tìm thấy bây giờ. Họ đã kiểm tra mã byte được tạo và có thêm một bước. Sự khác biệt quan sát được phụ thuộc rất nhiều vào người thử nghiệm (máy, SO ..), đôi khi chỉ tìm thấy sự khác biệt rất nhỏ.
joaquin

3
Trên 3.x, cả hai đều tạo mã lưu mã byte giống hệt nhau cho một số mã không thể truy cập ( LOAD_CONST(None); RETURN_VALUE- nhưng như đã nêu, nó không bao giờ đạt được) vào cuối with_else. Tôi rất nghi ngờ mã chết làm cho một chức năng nhanh hơn. Ai đó có thể cung cấp distrên 2.7?

4
Tôi đã không thể tái tạo điều này. Chức năng với elseFalsechậm nhất trong số tất cả (152ns). Nhanh thứ hai là Truekhông có else(143ns) và hai cái khác về cơ bản là giống nhau (137ns và 138ns). Tôi đã không sử dụng tham số mặc định và đo nó bằng %timeitiPython.
rplnt

Tôi không thể sao chép các thời gian đó, đôi khi with_else nhanh hơn, đôi khi đây là phiên bản without_else, có vẻ như chúng khá giống nhau đối với tôi ...
Cédric Julien

1
Đã thêm kết quả tháo gỡ. Tôi đang sử dụng Ubuntu 11.10, 64-bit, stock Python 2.7 - cùng cấu hình với @mac. Tôi cũng đồng tình rằng đó with_elselà nhanh hơn quan sát.
Chris Morgan

Câu trả lời:


387

Đây là một phỏng đoán thuần túy và tôi chưa tìm ra một cách dễ dàng để kiểm tra xem nó có đúng không, nhưng tôi có một lý thuyết cho bạn.

Tôi đã thử mã của bạn và nhận được kết quả tương tự, without_else()liên tục chậm hơn một chút so với with_else():

>>> T(lambda : without_else()).repeat()
[0.42015745017874906, 0.3188967452567226, 0.31984281521812363]
>>> T(lambda : with_else()).repeat()
[0.36009842032996175, 0.28962249392031936, 0.2927151355828528]
>>> T(lambda : without_else(True)).repeat()
[0.31709728471076915, 0.3172671387005721, 0.3285821242644147]
>>> T(lambda : with_else(True)).repeat()
[0.30939889008243426, 0.3035132258429485, 0.3046679117038593]

Xem xét rằng mã byte là giống hệt nhau, sự khác biệt duy nhất là tên của hàm. Cụ thể, bài kiểm tra thời gian thực hiện tra cứu tên toàn cầu. Hãy thử đổi tên without_else()và sự khác biệt biến mất:

>>> def no_else(param=False):
    if param:
        return 1
    return 0

>>> T(lambda : no_else()).repeat()
[0.3359846013948413, 0.29025818923918223, 0.2921801513879245]
>>> T(lambda : no_else(True)).repeat()
[0.3810395594970828, 0.2969634408842694, 0.2960104566362247]

Tôi đoán là without_elsecó một xung đột băm với một cái gì đó khác globals()nên việc tìm kiếm tên toàn cầu chậm hơn một chút.

Chỉnh sửa : Một từ điển có 7 hoặc 8 phím có thể có 32 vị trí, do đó, trên cơ sở đó without_elsecó xung đột băm với __builtins__:

>>> [(k, hash(k) % 32) for k in globals().keys() ]
[('__builtins__', 8), ('with_else', 9), ('__package__', 15), ('without_else', 8), ('T', 21), ('__name__', 25), ('no_else', 28), ('__doc__', 29)]

Để làm rõ cách băm hoạt động:

__builtins__ băm đến -1196389688 làm giảm modulo kích thước bảng (32) có nghĩa là nó được lưu trữ trong khe số 8 của bảng.

without_elsebăm đến 505688136 làm giảm modulo 32 là 8 nên có va chạm. Để giải quyết tính toán Python này:

Bắt đầu với:

j = hash % 32
perturb = hash

Lặp lại điều này cho đến khi chúng tôi tìm thấy một khe miễn phí:

j = (5*j) + 1 + perturb;
perturb >>= 5;
use j % 2**i as the next table index;

cung cấp cho nó 17 để sử dụng làm chỉ mục tiếp theo. May mắn là nó miễn phí nên vòng lặp chỉ lặp lại một lần. Kích thước bảng băm là lũy thừa bằng 2, 2**ikích thước của bảng băm icũng là số bit được sử dụng từ giá trị băm j.

Mỗi đầu dò vào bảng có thể tìm thấy một trong những điều sau đây:

  • Vị trí trống, trong trường hợp đó, việc thăm dò dừng lại và chúng ta biết giá trị không có trong bảng.

  • Vị trí không được sử dụng nhưng đã được sử dụng trong quá khứ, trong trường hợp đó chúng tôi sẽ thử giá trị tiếp theo được tính như trên.

  • Vị trí đã đầy nhưng giá trị băm đầy đủ được lưu trữ trong bảng không giống với giá trị băm của khóa mà chúng tôi đang tìm kiếm (đó là những gì xảy ra trong trường hợp __builtins__so với without_else).

  • Vị trí đã đầy và có chính xác giá trị băm mà chúng ta muốn, sau đó Python kiểm tra xem khóa và đối tượng chúng ta đang tìm có phải là cùng một đối tượng không (trong trường hợp này chúng sẽ là vì các chuỗi ngắn có thể là định danh được đặt trong đó định danh giống hệt nhau sử dụng cùng một chuỗi chính xác).

  • Cuối cùng, khi vị trí đầy, hàm băm khớp chính xác, nhưng các khóa không phải là đối tượng giống hệt nhau, và sau đó Python sẽ thử so sánh chúng cho bằng nhau. Điều này tương đối chậm, nhưng trong trường hợp tra cứu tên thực sự không nên xảy ra.


9
@Chris, không có độ dài của chuỗi không đáng kể. Lần đầu tiên bạn băm một chuỗi, nó sẽ mất thời gian tỷ lệ thuận với độ dài chuỗi nhưng sau đó hàm băm được tính toán được lưu trong bộ đệm đối tượng để các giá trị băm tiếp theo là O (1).
Duncan

1
À, tôi không biết về bộ nhớ đệm, nhưng điều đó có ý nghĩa
Chris Eberle

9
Hấp dẫn! Tôi có thể gọi bạn là Sherlock không? ;) Dù sao, tôi hy vọng tôi sẽ không quên cung cấp cho bạn một số điểm bổ sung với tiền thưởng ngay khi câu hỏi đủ điều kiện.
Voo

4
@mac, không hẳn. Tôi sẽ thêm một chút về độ phân giải băm (sẽ đưa nó vào bình luận nhưng nó thú vị hơn tôi nghĩ).
Duncan

6
@Duncan - Cảm ơn bạn rất nhiều vì đã dành thời gian để minh họa quá trình băm. Câu trả lời đỉnh cao! :)
mac
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.