2011-11-09 45 views
79

Trong khi tối ưu hóa mã của tôi tôi nhận ra điều sau đây:Python: tại sao * và ** nhanh hơn/và sqrt()?

>>> from timeit import Timer as T 
>>> T(lambda : 1234567890/4.0).repeat() 
[0.22256922721862793, 0.20560789108276367, 0.20530295372009277] 
>>> from __future__ import division 
>>> T(lambda : 1234567890/4).repeat() 
[0.14969301223754883, 0.14155197143554688, 0.14141488075256348] 
>>> T(lambda : 1234567890 * 0.25).repeat() 
[0.13619112968444824, 0.1281130313873291, 0.12830305099487305] 

và cũng:

>>> from math import sqrt 
>>> T(lambda : sqrt(1234567890)).repeat() 
[0.2597470283508301, 0.2498021125793457, 0.24994492530822754] 
>>> T(lambda : 1234567890 ** 0.5).repeat() 
[0.15409398078918457, 0.14059877395629883, 0.14049601554870605] 

tôi giả sử nó đã làm với con đường python được thực hiện trong C, nhưng tôi tự hỏi, nếu ai sẽ chăm sóc để giải thích tại sao lại như vậy?

+0

Câu trả lời bạn đã chấp nhận cho câu hỏi của mình (mà tôi cho rằng câu trả lời cho câu hỏi thực sự của bạn) không liên quan nhiều đến tiêu đề câu hỏi của bạn. Bạn có thể chỉnh sửa nó để có một cái gì đó để làm với gấp liên tục? –

+1

@ZanLynx - Xin chào. Bạn có muốn làm rõ không? Tôi thấy rằng tiêu đề câu hỏi thể hiện chính xác những gì tôi muốn biết (tại sao X nhanh hơn Y) và câu trả lời tôi chọn chính xác ... Có vẻ như một trận đấu hoàn hảo với tôi ... nhưng có lẽ tôi đang nhìn cái gì đó? – mac

+8

Phép nhân và chức năng nguồn luôn nhanh hơn các phép chia và sqrt() vì bản chất của chúng. Các hoạt động của bộ phận và gốc thường phải sử dụng một loạt các phép tính gần đúng hơn và tốt hơn và không thể đi trực tiếp đến câu trả lời đúng như phép nhân có thể. –

Trả lời

112

Lý do (hơi bất ngờ) cho kết quả của bạn là Python có vẻ gấp các biểu thức liên tục liên quan đến phép nhân dấu chấm động và lũy thừa, nhưng không phân chia. math.sqrt() là một con thú khác hoàn toàn vì không có bytecode cho nó và nó liên quan đến một cuộc gọi chức năng.

Mở Python 2.6.5, đoạn code sau:

x1 = 1234567890.0/4.0 
x2 = 1234567890.0 * 0.25 
x3 = 1234567890.0 ** 0.5 
x4 = math.sqrt(1234567890.0) 

biên dịch với bytecode sau:

# x1 = 1234567890.0/4.0 
    4   0 LOAD_CONST    1 (1234567890.0) 
       3 LOAD_CONST    2 (4.0) 
       6 BINARY_DIVIDE  
       7 STORE_FAST    0 (x1) 

    # x2 = 1234567890.0 * 0.25 
    5   10 LOAD_CONST    5 (308641972.5) 
      13 STORE_FAST    1 (x2) 

    # x3 = 1234567890.0 ** 0.5 
    6   16 LOAD_CONST    6 (35136.418286444619) 
      19 STORE_FAST    2 (x3) 

    # x4 = math.sqrt(1234567890.0) 
    7   22 LOAD_GLOBAL    0 (math) 
      25 LOAD_ATTR    1 (sqrt) 
      28 LOAD_CONST    1 (1234567890.0) 
      31 CALL_FUNCTION   1 
      34 STORE_FAST    3 (x4) 

Như bạn có thể thấy, nhân và lũy thừa mất không có thời gian ở tất cả vì họ thực hiện lại khi mã được biên dịch. Phân chia mất nhiều thời gian hơn vì nó xảy ra trong thời gian chạy. Quảng trường gốc không chỉ là hoạt động tốn kém nhất về tính toán của bốn, nó cũng phát sinh các chi phí khác nhau mà những người khác không (tra cứu thuộc tính, gọi hàm vv).

Nếu bạn loại bỏ ảnh hưởng của gấp liên tục, có rất ít để tách nhân và chia:

In [16]: x = 1234567890.0 

In [17]: %timeit x/4.0 
10000000 loops, best of 3: 87.8 ns per loop 

In [18]: %timeit x * 0.25 
10000000 loops, best of 3: 91.6 ns per loop 

math.sqrt(x) thực sự là nhanh hơn một chút so với x ** 0.5, có lẽ vì đây là một trường hợp đặc biệt của thứ hai và có thể do đó được thực hiện một cách hiệu quả hơn, mặc dù chi phí chung:

In [19]: %timeit x ** 0.5 
1000000 loops, best of 3: 211 ns per loop 

In [20]: %timeit math.sqrt(x) 
10000000 loops, best of 3: 181 ns per loop 

chỉnh sửa 2011/11/16: liên tục biểu hiện gấp được thực hiện bởi pe Python bộ tối ưu hóa ephole. Các mã nguồn (peephole.c) chứa các bình luận sau giải thích lý do tại sao bộ phận liên tục không gấp:

case BINARY_DIVIDE: 
     /* Cannot fold this operation statically since 
      the result can depend on the run-time presence 
      of the -Qnew flag */ 
     return 0; 

Cờ -Qnew phép "phân chia đúng" quy định tại PEP 238.

+2

Có lẽ nó là "bảo vệ" chống lại chia-by-zero? – hugomg

+2

@missingno: Không rõ lý do tại sao bất kỳ "bảo vệ" như vậy là cần thiết vì cả hai đối số đều được biết tại thời gian biên dịch, và do đó là kết quả (là một trong số: + inf, -inf, NaN). – NPE

+1

có thể thử nghiệm 'from __future__ import' sử dụng phương pháp tương tự. – Simon

Các vấn đề liên quan