Python: tại sao * và ** nhanh hơn / và sqrt ()?


80

Trong khi tối ưu hóa mã của mình, tôi nhận ra những điều sau:

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

>>> 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 cho rằng nó liên quan đến cách python được triển khai trong C, nhưng tôi tự hỏi liệu có ai quan tâm giải thích tại sao lại như vậy không?


Câu trả lời mà bạn đã chấp nhận cho câu hỏi của mình (mà tôi cho là 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ó để làm gì đó với việc gấp liên tục không?
Zan Lynx

1
@ZanLynx - Chào bạn. Bạn có vui lòng 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 là ... Có vẻ như là một kết hợp hoàn hảo với tôi ... nhưng có lẽ tôi đang bỏ qua điều gì đó?
mac

8
Các hàm nhân và lũy thừa luôn nhanh hơn các hàm chia và sqrt () vì bản chất của chúng. Các phép toán chia và căn thường phải sử dụng một loạt các phép tính gần đúng và nhỏ hơn và không thể đi thẳng đến câu trả lời đúng như phép nhân có thể.
Zan Lynx

Tôi cảm thấy như tiêu đề câu hỏi nên nói điều gì đó về thực tế là tất cả các giá trị đều là hằng số theo nghĩa đen, hóa ra lại là chìa khóa cho câu trả lời. Trên phần cứng điển hình, phép nhân và cộng / trừ số nguyên và FP rất rẻ; số nguyên và div FP và FP sqrt, đều đắt tiền (có thể độ trễ kém hơn gấp 3 lần và thông lượng kém hơn 10 lần so với FP mul). (Hầu hết các CPU thực hiện các hoạt động này trong phần cứng dưới dạng các lệnh asm duy nhất, không giống như cube-root hoặc pow () hoặc bất cứ thứ gì).
Peter Cordes

1
Nhưng tôi sẽ không ngạc nhiên nếu trình thông dịch Python trên đầu vẫn làm giảm sự khác biệt giữa hướng dẫn mul và div asm. Thực tế thú vị: trên x86, phép chia FP thường có hiệu suất cao hơn phép chia số nguyên. ( agner.org/optimize ). Phép chia số nguyên 64 bit trên Intel Skylake có độ trễ là 42-95 chu kỳ, so với 26 chu kỳ cho số nguyên 32 bit, so với 14 chu kỳ cho FP chính xác kép. (Nhân số nguyên 64 bit là độ trễ 3 chu kỳ, đa số FP là 4). Sự khác biệt thông thậm chí còn lớn hơn (int / FP mul và thêm tất cả đều ít nhất một mỗi đồng hồ, nhưng bộ phận và sqrt không pipelined đầy đủ.)
Peter Cordes

Câu trả lời:


114

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

Trên Python 2.6.5, đoạn mã 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 thành các mã byte 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, phép nhân và phép lũy thừa hoàn toàn không mất thời gian vì chúng được thực hiện khi mã được biên dịch. Việc phân chia mất nhiều thời gian hơn vì nó xảy ra trong thời gian chạy. Căn bậc hai không chỉ là phép tính tốn kém nhất về mặt tính toán trong bốn phép toán, nó còn chịu nhiều chi phí khác nhau mà các phép khác không (tra cứu thuộc tính, gọi hàm, v.v.).

Nếu bạn loại bỏ ảnh hưởng của việc gấp liên tục, sẽ có rất ít thứ để tách phép 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ự nhanh hơn một chút x ** 0.5, có lẽ vì đây là trường hợp đặc biệt của trường hợp thứ hai và do đó có thể được thực hiện hiệu quả hơn, bất chấp các 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: Việc gấp biểu thức liên tục được thực hiện bởi trình tối ưu hóa lỗ nhìn trộm của Python. Mã nguồn ( peephole.c) chứa nhận xét sau giải thích tại sao phép chia hằng không được gấp lại:

    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ác -Qnewlá cờ cho phép "phân chia đúng" quy định tại PEP 238 .


2
Có lẽ nó đang "bảo vệ" chống lại sự chia cho-không?
ômomg

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

13
Việc gấp liên tục hoạt động với /Python 3 và với //Python 2 và 3. Vì vậy, rất có thể đây là kết quả của thực tế /có thể có các ý nghĩa khác nhau trong Python 2. Có thể khi gấp liên tục được thực hiện, vẫn chưa biết liệu from __future__ import divisioncó có hiệu lực?
interjay

4
@aix - 1./0.trong Python 2.7 không dẫn đến NaNnhưng a ZeroDivisionError.
detly

2
@Caridorc: Python được biên dịch thành bytecode (tệp .pyc), sau đó được diễn giải theo thời gian chạy Python. Bytecode không giống như Assembly / Machine Code (ví dụ như trình biên dịch C tạo ra). Mô-đun dis có thể được sử dụng để kiểm tra mã bytecode mà một đoạn mã nhất định biên dịch thành.
Tony Suffolk 66
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.