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

Lesezeit: 5 Minuten

Benutzeravatar von mac
Mac

Beim Optimieren meines Codes ist mir folgendes aufgefallen:

>>> 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 damit zu tun, wie Python in C implementiert ist, aber ich frage mich, ob jemand erklären möchte, warum das so ist?

  • 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 so bearbeiten, dass es etwas mit konstantem Falten zu tun hat?

    – Zan Luchs

    10. November 2011 um 1:37 Uhr

  • @ZanLynx – Hallo. Möchten Sie das klären? Ich finde, dass der Titel der Frage genau das ausdrückt, was ich wissen wollte (warum X schneller als Y ist) und dass die von mir ausgewählte Antwort genau das tut … Scheint mir perfekt zu passen … aber vielleicht übersehe ich etwas?

    – Mac

    10. November 2011 um 6:50 Uhr

  • 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 Annäherungen verwenden und können nicht direkt zur richtigen Antwort führen, wie dies bei einer Multiplikation der Fall ist.

    – Zan Luchs

    10. November 2011 um 17:34 Uhr

  • Ich denke, der Titel der Frage sollte etwas darüber aussagen, dass die Werte alle wörtliche Konstanten sind, was sich als Schlüssel zur Antwort herausstellt. Auf typischer Hardware sind Integer- und FP-Multiplikation und Addieren/Subtrahieren billig; integer und FP div und FP sqrt sind alle teuer (vielleicht 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 Würfelwurzel oder pow() oder was auch immer).

    – Peter Cordes

    22. September 2016 um 22:02 Uhr


  • Aber ich wäre nicht überrascht, wenn der Python-Interpreter-Overhead 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/optimieren). Eine 64-Bit-Integerteilung auf Intel Skylake hat eine Latenz von 42–95 Zyklen gegenüber 26 Zyklen für 32-Bit-Integer und 14 Zyklen für FP mit doppelter Genauigkeit. (64-Bit-Integer-Multiplikation ist 3 Zyklen Latenz, FP mul ist 4). Die Durchsatzunterschiede sind sogar noch größer (int/FP mul und add sind alle mindestens eins pro Takt, aber division und sqrt sind nicht vollständig gepipelined.)

    – Peter Cordes

    22. September 2016 um 22:07 Uhr


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

Unter Python 2.6.5 der folgende Code:

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

kompiliert zu den 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 Potenzierung überhaupt keine Zeit in Anspruch, da sie beim Kompilieren des Codes ausgeführt werden. Die Division dauert länger, da sie zur Laufzeit erfolgt. Die Quadratwurzel ist nicht nur die rechenintensivste der vier Operationen, sondern verursacht auch verschiedene Overheads, die die anderen nicht haben (Attributsuche, Funktionsaufruf usw.).

Wenn Sie den Effekt der konstanten Faltung eliminieren, gibt es wenig zu trennendes Multiplizieren und Dividieren:

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.5vermutlich weil es ein Sonderfall des letzteren ist 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

Bearbeiten 16.11.2011: Das Falten konstanter Ausdrücke wird vom Peephole-Optimierer von Python durchgeführt. Der Quellcode (peephole.c) enthält den folgenden Kommentar, der erklärt, warum die konstante Division nicht gefaltet wird:

    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 -Qnew Flag aktiviert “true division” definiert in PEP 238.

  • Vielleicht “schützt” es vor einer Division durch Null?

    – Umarmung

    9. November 2011 um 16:38 Uhr

  • @missingno: Mir ist unklar, warum ein solcher “Schutz” erforderlich wäre, da beide Argumente zur Kompilierzeit bekannt sind, ebenso wie das Ergebnis (das eines von: +inf, -inf, NaN ist).

    – NPE

    9. November 2011 um 16:44 Uhr

  • Ständiges Falten funktioniert mit / in Python 3 und mit // in Python 2 und 3. Dies ist höchstwahrscheinlich darauf zurückzuführen, dass / kann in Python 2 unterschiedliche Bedeutungen haben. Vielleicht ist bei konstanter Faltung noch nicht bekannt, ob from __future__ import division ist in Kraft?

    – zwischenjay

    9. November 2011 um 18:00 Uhr

  • @aix – 1./0. in Python 2.7 führt nicht zu NaN aber ein ZeroDivisionError.

    – dezent

    10. November 2011 um 2:02 Uhr

  • @Caridorc: Python wird in Bytecode (.pyc-Dateien) kompiliert, der dann von der Python-Laufzeit interpretiert wird. Bytecode ist nicht dasselbe wie Assembly/Machine Code (was zum Beispiel ein C-Compiler generiert). Das Modul dis kann verwendet werden, um den Bytecode zu untersuchen, in den ein bestimmtes Codefragment kompiliert wird.

    – Toni Suffolk 66

    15. April 2015 um 16:22 Uhr

1418220cookie-checkPython: Warum sind * und ** schneller als / und sqrt()?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy