Warum ist x ** 4.0 in Python 3 schneller als x ** 4?

164

Warum ist x**4.0schneller als x**4? Ich verwende CPython 3.5.2.

$ python -m timeit "for x in range(100):" " x**4.0"
  10000 loops, best of 3: 24.2 usec per loop

$ python -m timeit "for x in range(100):" " x**4"
  10000 loops, best of 3: 30.6 usec per loop

Ich habe versucht, die Kraft, die ich erhöht habe, zu ändern, um zu sehen, wie sie sich verhält. Wenn ich beispielsweise x auf 10 oder 16 erhöhe, springt sie von 30 auf 35, aber wenn ich als Schwimmer um 10,0 erhöhe, bewegt sie sich nur um 24.1 ~ 4.

Ich denke, es hat etwas mit Float-Konvertierung und Potenzen von 2 zu tun, aber ich weiß es nicht wirklich.

Ich habe festgestellt, dass in beiden Fällen Potenzen von 2 schneller sind, da diese Berechnungen für den Dolmetscher / Computer nativer / einfacher sind. Trotzdem bewegt es sich mit Schwimmern fast nicht. 2.0 => 24.1~4 & 128.0 => 24.1~4 aber 2 => 29 & 128 => 62


TigerhawkT3 wies darauf hin, dass dies nicht außerhalb der Schleife geschieht. Ich habe nachgesehen und die Situation tritt nur auf (nach dem, was ich gesehen habe), wenn die Basis angehoben wird. Irgendeine Idee dazu?

Arieljannai
quelle
11
Für das, was es wert ist: Python 2.7.13 ist für mich ein Faktor 2 ~ 3 schneller und zeigt das umgekehrte Verhalten: Ein ganzzahliger Exponent ist schneller als ein Gleitkomma-Exponent.
4
@Evert yup, ich habe 14 usec für x**4.0und 3,9 für x**4.
Dabadaba

Antworten:

161

Warum ist x**4.0 schneller als x**4in Python 3 * ?

Python 3- intObjekte sind vollwertige Objekte, die eine beliebige Größe unterstützen. Aus diesem Grund werden sie auf C-Ebene als solche behandelt (siehe, wie alle Variablen als PyLongObject *Typ-In deklariert werden long_pow). Dies macht ihre Potenzierung auch viel schwieriger und langwieriger, da Sie mit dem ob_digitArray herumspielen müssen, das es verwendet, um seinen Wert darzustellen, um es auszuführen. ( Quelle für die Mutigen. - Siehe auch : Grundlegendes zu Speicherzuweisung für große Zahlen in Python für mehr auf PyLongObject. S)

floatIm Gegensatz dazu können Python- Objekte double(mithilfe von PyFloat_AsDouble) in einen C- Typ umgewandelt werden, und Operationen können mit diesen nativen Typen ausgeführt werden . Das ist großartig , weil nach dem für relevante Ränder Fälle überprüft, es Python ermöglicht die Plattformenpow ( C ist pow, das ist ) , um die tatsächliche Potenzierung zu handhaben :

/* Now iv and iw are finite, iw is nonzero, and iv is
 * positive and not equal to 1.0.  We finally allow
 * the platform pow to step in and do the rest.
 */
errno = 0;
PyFPE_START_PROTECT("pow", return NULL)
ix = pow(iv, iw); 

wo ivund iwsind unsere originalen PyFloatObjects als C doubles.

Für das, was es wert ist: Python ist 2.7.13für mich ein Faktor 2~3schneller und zeigt das umgekehrte Verhalten.

Die vorherige Tatsache erklärt auch die Diskrepanz zwischen Python 2 und 3, daher dachte ich, ich würde diesen Kommentar auch ansprechen, weil er interessant ist.

In Python 2 verwenden Sie das alte intObjekt, das sich vom intObjekt in Python 3 unterscheidet (alle intObjekte in 3.x sind vom PyLongObjectTyp). In Python 2 gibt es eine Unterscheidung, die vom Wert des Objekts abhängt (oder, wenn Sie das Suffix verwenden L/l):

# Python 2
type(30)  # <type 'int'>
type(30L) # <type 'long'>

Die <type 'int'>Sie hier sehen , macht das gleiche floats tun , wird es sicher in eine C umgewandelt , long wenn Potenzierung auf sie durchgeführt wird (die int_powauch Hinweise der Compiler ‚em in einem Register , wenn es so tun, so dass könnte einen Unterschied machen) ::

static PyObject *
int_pow(PyIntObject *v, PyIntObject *w, PyIntObject *z)
{
    register long iv, iw, iz=0, ix, temp, prev;
/* Snipped for brevity */    

Dies ermöglicht einen guten Geschwindigkeitsgewinn.

Um zu sehen, wie träge <type 'long'>s im Vergleich zu <type 'int'>s sind, verschwindet der Geschwindigkeitsgewinn , wenn Sie den xNamen in longPython 2 in einen Aufruf eingeschlossen haben (was im Wesentlichen die Verwendung long_powwie in Python 3 erzwingt ):

# <type 'int'>
(python2)  python -m timeit "for x in range(1000):" " x**2"       
10000 loops, best of 3: 116 usec per loop
# <type 'long'> 
(python2)  python -m timeit "for x in range(1000):" " long(x)**2"
100 loops, best of 3: 2.12 msec per loop

Beachten Sie, dass, obwohl das eine Snippet das intzu transformiert , longwährend das andere nicht (wie von @pydsinger hervorgehoben), diese Besetzung nicht die treibende Kraft hinter der Verlangsamung ist. Die Umsetzung von long_powist. (Zeit die Aussagen nur mit long(x)zu sehen).

[...] es passiert nicht außerhalb der Schleife. [...] Irgendeine Idee dazu?

Dies ist CPythons Gucklochoptimierer, der die Konstanten für Sie faltet. In beiden Fällen erhalten Sie genau die gleichen Timings, da keine tatsächliche Berechnung zum Ermitteln des Ergebnisses der Potenzierung erfolgt, sondern nur das Laden von Werten:

dis.dis(compile('4 ** 4', '', 'exec'))
  1           0 LOAD_CONST               2 (256)
              3 POP_TOP
              4 LOAD_CONST               1 (None)
              7 RETURN_VALUE

Es wird ein identischer Bytecode generiert, '4 ** 4.'mit dem einzigen Unterschied, dass LOAD_CONSTder Float 256.0anstelle des int geladen wird 256:

dis.dis(compile('4 ** 4.', '', 'exec'))
  1           0 LOAD_CONST               3 (256.0)
              2 POP_TOP
              4 LOAD_CONST               2 (None)
              6 RETURN_VALUE

Die Zeiten sind also identisch.


* Alle oben genannten Punkte gelten ausschließlich für CPython, die Referenzimplementierung von Python. Andere Implementierungen funktionieren möglicherweise anders.

Dimitris Fasarakis Hilliard
quelle
Was auch immer es ist, es hängt mit der Schleife über a zusammen range, da nur die **Operation selbst keinen Unterschied zwischen ganzen Zahlen und Gleitkommazahlen ergibt.
TigerhawkT3
Der Unterschied tritt nur beim Nachschlagen einer Variablen auf ( 4**4ist genauso schnell wie 4**4.0), und diese Antwort berührt das überhaupt nicht.
TigerhawkT3
1
Konstanten werden jedoch bei TigerhawkT3 ( dis(compile('4 ** 4', '', 'exec'))) gefaltet, sodass die Zeit genau gleich sein sollte.
Dimitris Fasarakis Hilliard
Ihre letzten Timings scheinen nicht zu zeigen, was Sie sagen. long(x)**2.ist immer noch schneller als long(x)**2um den Faktor 4-5. (Nicht einer der Downvoter)
Graipherie
3
@ mbomb007 Die Eliminierung des <type 'long'>Typs in Python 3 erklärt sich wahrscheinlich aus den Bemühungen, die Sprache zu vereinfachen. Wenn Sie einen Typ zur Darstellung von Ganzzahlen haben können, ist dieser übersichtlicher als zwei (und Sie müssen sich bei Bedarf Gedanken über die Konvertierung von einem in den anderen machen, Benutzer werden verwirrt usw.). Die Geschwindigkeitsverstärkung ist zweitrangig. Der Begründungsteil von PEP 237 bietet auch weitere Einblicke.
Dimitris Fasarakis Hilliard
25

Wenn wir uns den Bytecode ansehen, können wir sehen, dass die Ausdrücke rein identisch sind. Der einzige Unterschied ist der Typ einer Konstante, für die ein Argument vorliegt BINARY_POWER. Es liegt also mit Sicherheit daran, intdass auf der ganzen Linie in eine Gleitkommazahl konvertiert wird.

>>> def func(n):
...    return n**4
... 
>>> def func1(n):
...    return n**4.0
... 
>>> from dis import dis
>>> dis(func)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (4)
              6 BINARY_POWER
              7 RETURN_VALUE
>>> dis(func1)
  2           0 LOAD_FAST                0 (n)
              3 LOAD_CONST               1 (4.0)
              6 BINARY_POWER
              7 RETURN_VALUE

Update: Schauen wir uns Objects / abstract.c im CPython-Quellcode an:

PyObject *
PyNumber_Power(PyObject *v, PyObject *w, PyObject *z)
{
    return ternary_op(v, w, z, NB_SLOT(nb_power), "** or pow()");
}

PyNumber_PowerAnrufe ternary_op, die zu lang sind, um sie hier einzufügen . Hier ist der Link .

Es nennt den nb_powerSlot von x, yals Argument übergeben.

Schließlich sehen wir in float_pow()Zeile 686 von Objects / floatobject.c , dass Argumente doubleunmittelbar vor der eigentlichen Operation in ein C konvertiert werden:

static PyObject *
float_pow(PyObject *v, PyObject *w, PyObject *z)
{
    double iv, iw, ix;
    int negate_result = 0;

    if ((PyObject *)z != Py_None) {
        PyErr_SetString(PyExc_TypeError, "pow() 3rd argument not "
            "allowed unless all arguments are integers");
        return NULL;
    }

    CONVERT_TO_DOUBLE(v, iv);
    CONVERT_TO_DOUBLE(w, iw);
    ...
leovp
quelle
1
@ Jean-FrançoisFabre Ich glaube, das liegt an der ständigen Faltung.
Dimitris Fasarakis Hilliard
2
Ich denke, die Implikation, dass es eine Konvertierung gibt und sie "auf jeden Fall" nicht anders gehandhabt werden, ist ein bisschen langwierig ohne Quelle.
Miradulo
1
@Mitch - Zumal es in diesem speziellen Code keinen Unterschied in der Ausführungszeit für diese beiden Operationen gibt. Der Unterschied ergibt sich nur mit der Schleife des OP. Diese Antwort führt zu Schlussfolgerungen.
TigerhawkT3
2
Warum schaust du nur, float_powwenn das nicht einmal für den langsamen Fall läuft?
user2357112 unterstützt Monica
2
@ TigerhawkT3: 4**4und 4**4.0konstant gefaltet werden. Das ist ein völlig separater Effekt.
user2357112 unterstützt Monica
-1

Weil einer richtig ist, ist ein anderer Annäherung.

>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625
Veky
quelle
Ich weiß nicht, warum dieser Downvoter downvotiert hat, aber ich habe es getan, weil diese Antwort die Frage nicht beantwortet. Nur weil etwas korrekt ist, bedeutet dies keineswegs, dass es schneller oder langsamer ist. Einer ist langsamer als der andere, da einer mit C-Typen arbeiten kann, während der andere mit Python-Objekten arbeiten muss.
Dimitris Fasarakis Hilliard
1
Danke für die Erklärung. Nun, ich dachte wirklich, es sei offensichtlich, dass es schneller ist, nur die Annäherung einer Zahl an etwa 12 Ziffern zu berechnen, als alle genau zu berechnen. Der einzige Grund, warum wir Näherungswerte verwenden, ist, dass sie schneller zu berechnen sind, oder?
Veky