Python: Warum sind * und ** schneller als / und sqrt ()?

80

Bei der Optimierung meines Codes wurde Folgendes festgestellt:

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

und auch:

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

Ich nehme an, es hat mit der Art und Weise zu tun, wie Python in C implementiert ist, aber ich frage mich, ob jemand erklären möchte, warum das so ist.

Mac
quelle
Die Antwort, die Sie für Ihre Frage akzeptiert haben (von der ich annehme, dass sie Ihre eigentliche Frage beantwortet), hat nicht viel mit Ihrem Fragentitel zu tun. Könnten Sie es bearbeiten, um etwas mit ständigem Falten zu tun zu haben?
Zan Lynx
1
@ ZanLynx - Hallo. Würde es Ihnen etwas ausmachen zu klären? Ich finde, dass der Fragentitel genau das ausdrückt, was ich wissen wollte (warum X schneller als Y ist) und dass die Antwort, die ich ausgewählt habe, genau das tut ... Scheint perfekt zu mir zu passen ... aber vielleicht übersehen ich etwas?
Mac
8
Multiplikations- und Potenzfunktionen sind aufgrund ihrer Natur immer schneller als Divisions- und sqrt () -Funktionen. Divisions- und Wurzeloperationen müssen im Allgemeinen eine Reihe von immer feineren Näherungen verwenden und können nicht wie die Multiplikation direkt zur richtigen Antwort gelangen.
Zan Lynx
Ich denke, der Titel der Frage sollte etwas über die Tatsache aussagen, dass die Werte alle wörtliche Konstanten sind, was sich als Schlüssel zur Antwort herausstellt. Bei typischer Hardware sind Integer und FP multiplizieren und addieren / subtrahieren billig. Integer und FP div sowie FP sqrt sind alle teuer (möglicherweise 3x schlechtere Latenz und 10x schlechterer Durchsatz als FP mul). (Die meisten CPUs implementieren diese Operationen in Hardware als einzelne ASM-Anweisungen, im Gegensatz zu Cube-Root oder pow () oder was auch immer).
Peter Cordes
1
Aber ich wäre nicht überrascht, wenn der Overhead des Python-Interpreters den Unterschied zwischen mul- und div asm-Anweisungen immer noch in den Schatten stellen würde. Unterhaltsame Tatsache: Auf x86 ist die FP-Division normalerweise leistungsstärker als die Integer-Division. ( agner.org/optimize ). Eine 64-Bit-Ganzzahldivision unter Intel Skylake hat eine Latenz von 42-95 Zyklen gegenüber 26 Zyklen bei 32-Bit-Ganzzahlen gegenüber 14 Zyklen bei FP mit doppelter Genauigkeit. (64-Bit-Ganzzahlmultiplikation ist 3-Zyklus-Latenz, FP-Mul ist 4). Die Durchsatzunterschiede sind noch größer (int / FP mul und add sind alle mindestens eins pro Takt, aber Division und sqrt sind nicht vollständig per Pipeline verbunden.)
Peter Cordes

Antworten:

114

Der (etwas unerwartete) Grund für Ihre Ergebnisse ist, dass Python konstante Ausdrücke zu falten scheint, die Gleitkomma-Multiplikation und Exponentiation beinhalten, aber keine Division. math.sqrt()ist ein ganz anderes Tier, da es keinen Bytecode dafür gibt und es einen Funktionsaufruf beinhaltet.

In Python 2.6.5 den folgenden Code:

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

Kompiliert zu folgenden Bytecodes:

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

Wie Sie sehen können, nehmen Multiplikation und Exponentiation überhaupt keine Zeit in Anspruch, da sie beim Kompilieren des Codes abgeschlossen sind. Die Teilung dauert länger, da sie zur Laufzeit erfolgt. Die Quadratwurzel ist nicht nur die rechenintensivste der vier Operationen, sondern verursacht auch verschiedene Gemeinkosten, die die anderen nicht verursachen (Attributsuche, Funktionsaufruf usw.).

Wenn Sie den Effekt der konstanten Faltung eliminieren, gibt es wenig, um Multiplikation und Division zu trennen:

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)ist tatsächlich ein bisschen schneller als x ** 0.5, vermutlich weil es sich um einen Sonderfall handelt und daher trotz des Overheads effizienter durchgeführt werden kann:

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

edit 2011-11-16: Das Falten konstanter Ausdrücke wird vom Python-Gucklochoptimierer durchgeführt. Der Quellcode ( peephole.c) enthält den folgenden Kommentar, der erklärt, warum die konstante Division nicht gefaltet ist:

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

Das -QnewFlag aktiviert die in PEP 238 definierte "wahre Teilung" .

NPE
quelle
2
Vielleicht "schützt" es vor der Division durch Null?
Hugomg
2
@missingno: Es ist mir unklar, warum ein solcher "Schutz" notwendig wäre, da beide Argumente zur Kompilierungszeit bekannt sind, ebenso wie das Ergebnis (eines von: + inf, -inf, NaN).
NPE
13
Konstantes Falten funktioniert /in Python 3 und //in Python 2 und 3. Dies ist also höchstwahrscheinlich auf die Tatsache zurückzuführen, dass /es in Python 2 unterschiedliche Bedeutungen haben kann. Wenn das konstante Falten durchgeführt wird, ist möglicherweise noch nicht bekannt, ob dies der Fall from __future__ import divisionist in der Tat?
Zwischen
4
@aix - 1./0.in Python 2.7 ergibt sich nicht NaNaber a ZeroDivisionError.
Detly
2
@Caridorc: Python wird in Bytecode (.pyc-Dateien) kompiliert, der dann von der Python-Laufzeit interpretiert wird. Bytecode ist nicht dasselbe wie Assembly- / Maschinencode (was beispielsweise ein C-Compiler generiert). Das dis-Modul kann verwendet werden, um den Bytecode zu untersuchen, zu dem ein bestimmtes Codefragment kompiliert wird.
Tony Suffolk 66