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?
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.5
vermutlich 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.
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