Python: dlaczego * i ** są szybsze niż / i sqrt ()?


80

Podczas optymalizacji kodu zdałem sobie sprawę z następujących kwestii:

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

i również:

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

Zakładam, że ma to związek ze sposobem implementacji Pythona w C, ale zastanawiam się, czy ktoś miałby ochotę wyjaśnić, dlaczego tak jest?


Odpowiedź, którą zaakceptowałeś na swoje pytanie (która, jak zakładam, odpowiada na twoje prawdziwe pytanie) nie ma wiele wspólnego z tytułem pytania. Czy możesz go edytować, aby mieć coś wspólnego z ciągłym zwijaniem?
Zan Lynx,

1
@ZanLynx - Cześć. Czy mógłbyś to wyjaśnić? Uważam, że tytuł pytania dokładnie wyraża to, co chciałem wiedzieć (dlaczego X jest szybsze niż Y) i że wybrana przeze mnie odpowiedź właśnie to ... Wydaje mi się idealnie pasująca do mnie ... ale może coś przeoczę?
Mac

8
Funkcje mnożenia i potęgi są zawsze szybsze niż dzielenie i funkcje sqrt () ze względu na swoją naturę. Operacje dzielenia i pierwiastka generalnie muszą wykorzystywać serię dokładniejszych i dokładniejszych przybliżeń i nie mogą bezpośrednio prowadzić do właściwej odpowiedzi, tak jak może to być mnożenie.
Zan Lynx

Wydaje mi się, że tytuł pytania powinien coś mówić o tym, że wszystkie wartości są stałymi dosłownie, co okazuje się kluczem do odpowiedzi. Na typowym sprzęcie mnożenie i dodawanie / odejmowanie liczb całkowitych i FP jest tanie; integer i FP div oraz FP sqrt są drogie (być może 3x gorsze opóźnienie i 10x gorsza przepustowość niż FP mul). (Większość procesorów implementuje te operacje sprzętowo jako pojedyncze instrukcje asm, w przeciwieństwie do cube-root, pow () lub cokolwiek innego).
Peter Cordes

1
Ale nie zdziwiłbym się, gdyby narzut interpretera Pythona nadal przyćmiał różnicę między instrukcjami mul i div asm. Ciekawostka: na x86 dzielenie FP jest zwykle wydajniejsze niż dzielenie liczb całkowitych. ( agner.org/optimize ). 64-bitowe dzielenie całkowitoliczbowe w Intel Skylake ma opóźnienie 42-95 cykli, w porównaniu z 26 cyklami dla 32-bitowych liczb całkowitych, w porównaniu z 14 cyklami dla podwójnej precyzji FP. (64-bitowe mnożenie liczb całkowitych to opóźnienie 3 cykli, FP mul to 4). Różnice w przepustowości są jeszcze większe (int / FP mul i add to co najmniej jeden na zegar, ale dzielenie i sqrt nie są w pełni potokowe.)
Peter Cordes

Odpowiedzi:


114

(Dość nieoczekiwany) powód twoich wyników jest taki, że Python wydaje się składać stałe wyrażenia obejmujące mnożenie i potęgowanie zmiennoprzecinkowe, ale nie dzielenie. math.sqrt()jest zupełnie inną bestią, ponieważ nie ma dla niej kodu bajtowego i zawiera wywołanie funkcji.

W Pythonie 2.6.5 następujący kod:

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

kompiluje się do następujących kodów bajtowych:

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

Jak widać, mnożenie i potęgowanie nie zajmuje dużo czasu, ponieważ są one wykonywane podczas kompilacji kodu. Dzielenie trwa dłużej, ponieważ dzieje się to w czasie wykonywania. Pierwiastek kwadratowy jest nie tylko najbardziej kosztowną obliczeniowo operacją z tych czterech, ale także wiąże się z różnymi kosztami narzutów, których nie mają inne (wyszukiwanie atrybutów, wywołanie funkcji itp.).

Jeśli wyeliminujesz efekt ciągłego zwijania, niewiele jest do oddzielenia mnożenia i dzielenia:

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)jest w rzeczywistości trochę szybszy niż x ** 0.5, prawdopodobnie dlatego, że jest to szczególny przypadek tego ostatniego i dlatego można go wykonać wydajniej, pomimo kosztów ogólnych:

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

edycja 2011-11-16: Zwijanie wyrażeń stałych jest wykonywane przez optymalizator wizjera Pythona. Kod źródłowy ( peephole.c) zawiera następujący komentarz wyjaśniający, dlaczego dzielenie stałe nie jest zawijane:

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

-QnewFlaga umożliwia podział „prawdziwą” zdefiniowanego w PEP 238 .


2
Może to „ochrona” przed dzieleniem przez zero?
hugomg

2
@missingno: Nie jest dla mnie jasne, dlaczego taka „ochrona” byłaby konieczna, skoro oba argumenty są znane w czasie kompilacji, podobnie jak wynik (który jest jednym z: + inf, -inf, NaN).
NPE

13
Stałe zwijanie działa /w Pythonie 3, //aw Pythonie 2 i 3. Najprawdopodobniej jest to spowodowane faktem, że /w Pythonie 2 może mieć różne znaczenia. Może po wykonaniu stałego zwijania nie wiadomo jeszcze, czy from __future__ import divisionjest w efekcie?
interjay

4
@aix - 1./0.w Pythonie 2.7 nie powoduje, NaNale plik ZeroDivisionError.
beztrosko

2
@Caridorc: Python jest kompilowany do kodu bajtowego (pliki .pyc), który jest następnie interpretowany przez środowisko wykonawcze Pythona. Kod bajtowy to nie to samo, co kod asemblacyjny / maszynowy (co na przykład generuje kompilator C). Moduł dis może służyć do sprawdzania kodu bajtowego, do którego kompiluje się dany fragment kodu.
Tony Suffolk 66
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.