Perché x ** 4.0 è più veloce di x ** 4 in Python 3?


164

Perché è x**4.0più veloce di x**4? Sto usando 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

Ho provato a cambiare la potenza che ho sollevato per vedere come agisce, e per esempio se alzo x alla potenza di 10 o 16 sta saltando da 30 a 35, ma se sto aumentando di 10.0 come float, è solo in movimento circa 24,1 ~ 4.

Immagino che abbia qualcosa a che fare con la conversione float e con potenze di 2 forse, ma non lo so davvero.

Ho notato che in entrambi i casi le potenze di 2 sono più veloci, immagino poiché quei calcoli sono più nativi / facili per l'interprete / il computer. Ma ancora, con i galleggianti non si muove quasi. 2.0 => 24.1~4 & 128.0 => 24.1~4 ma 2 => 29 & 128 => 62


TigerhawkT3 ha sottolineato che non accade al di fuori del ciclo. Ho controllato e la situazione si verifica (da quello che ho visto) solo quando la base viene sollevata. Qualche idea a riguardo?


11
Per quello che vale: Python 2.7.13 per me è un fattore 2 ~ 3 più veloce e mostra il comportamento inverso: un esponente intero è più veloce di un esponente in virgola mobile.

4
@Evidente, ho 14 usec per x**4.0e 3.9 per x**4.
Dabadaba,

Risposte:


161

Perché è x**4.0 più veloce che x**4in Python 3 * ?

Gli intoggetti Python 3 sono un oggetto completo progettato per supportare una dimensione arbitraria; per questo motivo, vengono gestiti come tali a livello C (vedere come tutte le variabili vengono dichiarate come PyLongObject *tipo in long_pow). Questo rende anche la loro esponenziazione molto più complicata e noiosa poiché è necessario giocare con l' ob_digitarray che utilizza per rappresentare il suo valore per eseguirlo. ( Sorgente per i più coraggiosi. Vedi: Comprensione dell'allocazione di memoria per interi di grandi dimensioni in Python per ulteriori informazioni su PyLongObjects.)

Gli floatoggetti Python , al contrario, possono essere trasformati in un doubletipo C (usando PyFloat_AsDouble) e le operazioni possono essere eseguite usando quei tipi nativi . Questo è fantastico perché, dopo aver verificato i casi limite rilevanti, consente a Python di utilizzare le piattaformepow ( C pow, cioè ) per gestire l'esponenziale effettivo:

/* 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); 

dove ive iwsono i nostri originali PyFloatObjectcome C doubles.

Per quello che vale: Python 2.7.13per me è un fattore 2~3più veloce e mostra il comportamento inverso.

Il fatto precedente spiega anche la discrepanza tra Python 2 e 3, quindi ho pensato di affrontare anche questo commento perché è interessante.

In Python 2, stai usando il vecchio intoggetto che differisce intdall'oggetto in Python 3 (tutti gli intoggetti in 3.x sono di PyLongObjecttipo). In Python 2, c'è una distinzione che dipende dal valore dell'oggetto (o, se si utilizza il suffisso L/l):

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

Il <type 'int'>che vedete qui fa la stessa cosa floats lo fanno , si ottiene in modo sicuro trasformata in C long quando elevamento a potenza viene eseguita su di esso (Il int_powaccenna anche al compilatore di mettere 'em in un registro, se può farlo, in modo che possa fare la differenza) :

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

questo consente un buon guadagno di velocità.

Per vedere come le <type 'long'>s lente sono in confronto a <type 'int'>s, se hai racchiuso il xnome in una longchiamata in Python 2 (essenzialmente costringendolo a usarlo long_powcome in Python 3), il guadagno di velocità scompare:

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

Prendi nota che, sebbene uno snippet trasformi il intto longmentre l'altro no (come sottolineato da @pydsinger), questo cast non è la forza che contribuisce al rallentamento. L'implementazione di long_powè. (Tempo le dichiarazioni solo con long(x)per vedere).

[...] non accade al di fuori del ciclo. [...] Qualche idea a riguardo?

Questo è l'ottimizzatore spioncino di CPython che piega le costanti per te. Si ottengono gli stessi tempi esatti in entrambi i casi poiché non esiste un calcolo effettivo per trovare il risultato dell'espiazione, solo caricamento di valori:

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

Il codice byte identico viene generato per '4 ** 4.'con l'unica differenza che LOAD_CONSTcarica il float 256.0anziché int 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

Quindi i tempi sono identici.


* Tutto quanto sopra si applica esclusivamente a CPython, l'implementazione di riferimento di Python. Altre implementazioni potrebbero funzionare diversamente.


Qualunque cosa sia, è correlata al loop su a range, poiché solo la temporizzazione **dell'operazione stessa non produce alcuna differenza tra numeri interi e float.
TigerhawkT3,

La differenza appare solo quando si cerca una variabile ( 4**4è altrettanto veloce 4**4.0) e questa risposta non la tocca affatto.
TigerhawkT3

1
Ma le costanti verranno piegate @ TigerhawkT3 ( dis(compile('4 ** 4', '', 'exec'))), quindi il tempo dovrebbe essere esattamente lo stesso.
Dimitris Fasarakis Hilliard,

I tuoi ultimi tempi sembrano non mostrare quello che dici. long(x)**2.è ancora più veloce di long(x)**2un fattore 4-5. (Non uno dei downvoter, però)
Graipher

3
@ mbomb007 l'eliminazione del <type 'long'>tipo in Python 3 è probabilmente spiegata dagli sforzi fatti per semplificare il linguaggio. Se è possibile avere un tipo per rappresentare numeri interi, è più gestibile di due (e preoccuparsi di convertirsi da uno all'altro quando necessario, gli utenti si confondono, ecc.). Il guadagno di velocità è secondario a quello. La sezione logica di PEP 237 offre anche ulteriori approfondimenti.
Dimitris Fasarakis Hilliard,

25

Se guardiamo il bytecode, possiamo vedere che le espressioni sono puramente identiche. L'unica differenza è un tipo di costante che sarà un argomento di BINARY_POWER. Quindi è sicuramente dovuto al fatto di intessere convertito in un numero in virgola mobile lungo la linea.

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

Aggiornamento: diamo un'occhiata a Objects / abstract.c nel codice sorgente di CPython:

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

PyNumber_Powerchiamate ternary_op, che è troppo lungo per incollare qui, quindi ecco il link .

Chiama lo nb_powerslot di x, passando ycome argomento.

Infine, nella float_pow()riga 686 di Oggetti / floatobject.c vediamo che gli argomenti vengono convertiti in una C doubleproprio prima dell'operazione effettiva:

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);
    ...

1
@ Jean-FrançoisFabre Credo che ciò sia dovuto al costante ribaltamento.
Dimitris Fasarakis Hilliard,

2
Penso che l'implicazione che ci sia una conversione e che non siano gestiti in modo diverso lungo la linea "certamente" è un po 'allungata senza una fonte.
miradulo,

1
@Mitch - Soprattutto perché, in questo particolare codice, non ci sono differenze nei tempi di esecuzione di queste due operazioni. La differenza sorge solo con il ciclo dell'OP. Questa risposta sta saltando alle conclusioni.
TigerhawkT3,

2
Perché guardi solo float_powquando questo non funziona nemmeno per il caso lento?
user2357112 supporta Monica il

2
@ TigerhawkT3: 4**4e 4**4.0ottenere una piega costante. Questo è un effetto completamente separato.
user2357112 supporta Monica il

-1

Perché uno è corretto, un altro è l'approssimazione.

>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625

Non so perché quel downvoter abbia declassato, ma l'ho fatto perché questa risposta non risponde alla domanda. Solo perché qualcosa è corretto non implica in alcun modo che sia più veloce o più lento. Uno è più lento dell'altro perché uno può lavorare con i tipi C mentre l'altro deve lavorare con gli oggetti Python.
Dimitris Fasarakis Hilliard

1
Grazie per la spiegazione. Bene, ho davvero pensato che fosse ovvio che è più veloce calcolare solo l'approssimazione di un numero a circa 12 cifre, piuttosto che calcolarli tutti esattamente. Dopotutto, l'unico motivo per cui usiamo le approssimazioni è che sono più veloci da calcolare, giusto?
Veky,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.