Pourquoi x ** 4.0 est-il plus rapide que x ** 4 dans Python 3?

164

Pourquoi est x**4.0plus rapide que x**4? J'utilise 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

J'ai essayé de changer la puissance que j'ai augmentée pour voir comment il agit, et par exemple si j'élève x à la puissance de 10 ou 16, il passe de 30 à 35, mais si j'élève de 10,0 en tant que flotteur, c'est juste en mouvement environ 24,1 ~ 4.

Je suppose que cela a quelque chose à voir avec la conversion de flotteurs et les puissances de 2 peut-être, mais je ne sais pas vraiment.

J'ai remarqué que dans les deux cas, les puissances de 2 sont plus rapides, je suppose que ces calculs sont plus natifs / faciles pour l'interprète / l'ordinateur. Mais quand même, avec des flotteurs, il ne bouge presque pas. 2.0 => 24.1~4 & 128.0 => 24.1~4 mais 2 => 29 & 128 => 62


TigerhawkT3 souligné que cela ne se produit pas en dehors de la boucle. J'ai vérifié et la situation ne se produit (d'après ce que j'ai vu) que lorsque la base se soulève. Une idée à ce sujet?

arieljannai
la source
11
Pour ce que ça vaut: Python 2.7.13 pour moi est un facteur 2 ~ 3 plus rapide, et montre le comportement inverse: un exposant entier est plus rapide qu'un exposant à virgule flottante.
4
@Evert yup, j'ai 14 usec pour x**4.0et 3,9 pour x**4.
dabadaba le

Réponses:

161

Pourquoi est-il x**4.0 plus rapide qu'en x**4Python 3 * ?

Les intobjets Python 3 sont un objet à part entière conçu pour prendre en charge une taille arbitraire; de ce fait, ils sont traités comme tels au niveau C (voir comment toutes les variables sont déclarées comme PyLongObject *type in long_pow). Cela rend également leur exponentiation beaucoup plus délicate et fastidieuse car vous devez jouer avec le ob_digittableau qu'il utilise pour représenter sa valeur pour l'exécuter. ( Source pour les courageux. - Voir: Comprendre l'allocation de mémoire pour les grands entiers en Python pour plus d'informations sur les PyLongObjects.)

Les floatobjets Python , au contraire, peuvent être transformés en doubletype C (en utilisant PyFloat_AsDouble) et les opérations peuvent être effectuées en utilisant ces types natifs . C'est génial car, après avoir vérifié les cas limites pertinents, cela permet à Python d' utiliser les plates-formespow ( C pow, c'est-à-dire ) pour gérer l'exponentiation réelle:

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

ivet iwsont nos originaux PyFloatObjectcomme C doubles.

Pour ce que ça vaut: Python 2.7.13pour moi est un facteur 2~3plus rapide et montre le comportement inverse.

Le fait précédent explique aussi l'écart entre Python 2 et 3 donc, j'ai pensé que je répondrais aussi à ce commentaire car il est intéressant.

Dans Python 2, vous utilisez l'ancien intobjet qui diffère de l' intobjet dans Python 3 (tous les intobjets dans 3.x sont de PyLongObjecttype). Dans Python 2, il existe une distinction qui dépend de la valeur de l'objet (ou, si vous utilisez le suffixe L/l):

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

Le <type 'int'>voyez - vous ici fait la même chose floatne s , il s'en toute sécurité convertie en C long lorsque exponentiation est effectuée sur elle (le int_powlaisse aussi le compilateur de mettre « em dans un registre si elle peut le faire, de sorte que pourrait faire une différence) :

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

cela permet un bon gain de vitesse.

Pour voir à quel point <type 'long'>s est lent par rapport à <type 'int'>s, si vous enveloppez le xnom dans un longappel en Python 2 (le forçant essentiellement à l'utiliser long_powcomme dans Python 3), le gain de vitesse disparaît:

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

Notez que, bien que l'un des extraits de code transforme le inten longalors que l'autre ne le fait pas (comme l'a souligné @pydsinger), ce casting n'est pas la force contributive du ralentissement. La mise en œuvre de long_powest. (Chronométrez les déclarations uniquement avec long(x)pour voir).

[...] cela ne se produit pas en dehors de la boucle. [...] Une idée à ce sujet?

Il s'agit de l'optimiseur de judas de CPython pliant les constantes pour vous. Vous obtenez le même timing exact dans les deux cas puisqu'il n'y a pas de calcul réel pour trouver le résultat de l'exponentiation, seulement le chargement des valeurs:

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

Un octet-code identique est généré pour, '4 ** 4.'la seule différence étant que le LOAD_CONSTcharge le float 256.0au lieu de l'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

Les temps sont donc identiques.


* Tout ce qui précède s'applique uniquement à CPython, l'implémentation de référence de Python. D'autres implémentations peuvent fonctionner différemment.

Dimitris Fasarakis Hilliard
la source
Quoi qu'il en soit, il est lié à la boucle sur a range, car le fait de ne chronométrer que l' **opération elle-même ne donne aucune différence entre les entiers et les flottants.
TigerhawkT3
La différence n'apparaît que lors de la recherche d'une variable ( 4**4est tout aussi rapide que 4**4.0), et cette réponse ne touche pas du tout à cela.
TigerhawkT3
1
Mais, les constantes seront repliées @ TigerhawkT3 ( dis(compile('4 ** 4', '', 'exec'))) donc l'heure devrait être exactement la même.
Dimitris Fasarakis Hilliard
Vos derniers horaires ne semblent pas montrer ce que vous dites. long(x)**2.est toujours plus rapide que long(x)**2par un facteur de 4-5. (Pas l'un des
votes négatifs
3
@ mbomb007 l'élimination du <type 'long'>type en Python 3 s'explique probablement par les efforts faits pour simplifier le langage. Si vous pouvez avoir un type pour représenter des entiers, il est plus gérable que deux (et vous inquiétez de la conversion de l'un à l'autre lorsque cela est nécessaire, les utilisateurs sont confus, etc.). Le gain de vitesse est secondaire à cela. La section sur la justification de la PEP 237 offre également un aperçu supplémentaire.
Dimitris Fasarakis Hilliard
25

Si nous regardons le bytecode, nous pouvons voir que les expressions sont purement identiques. La seule différence est un type de constante qui sera un argument de BINARY_POWER. C'est donc très certainement dû à une intconversion en nombre à virgule flottante sur la ligne.

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

Mise à jour: jetons un œil à Objects / abstract.c dans le code source CPython:

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

PyNumber_Powerappels ternary_op, ce qui est trop long à coller ici, alors voici le lien .

Il appelle la nb_powerfente de x, passanty comme argument.

Enfin, float_pow()à la ligne 686 de Objects / floatobject.c, nous voyons que les arguments sont convertis en C doublejuste avant l'opération réelle:

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
la source
1
@ Jean-FrançoisFabre Je crois que c'est dû au pliage constant.
Dimitris Fasarakis Hilliard
2
Je pense que l'implication qu'il y a une conversion et qu'ils ne sont pas traités différemment sur toute la ligne "très certainement" est un peu exagérée sans source.
miradulo
1
@Mitch - D'autant plus que, dans ce code particulier, il n'y a pas de différence dans le temps d'exécution de ces deux opérations. La différence se produit uniquement avec la boucle de l'OP. Cette réponse saute aux conclusions.
TigerhawkT3
2
Pourquoi ne regardez-vous que float_powlorsque cela ne fonctionne même pas pour le cas lent?
user2357112 prend en charge Monica le
2
@ TigerhawkT3: 4**4et 4**4.0obtenez constamment plié. C'est un effet entièrement distinct.
user2357112 prend en charge Monica le
-1

Parce que l'un est correct, un autre est l'approximation.

>>> 334453647687345435634784453567231654765 ** 4.0
1.2512490121794596e+154
>>> 334453647687345435634784453567231654765 ** 4
125124901217945966595797084130108863452053981325370920366144
719991392270482919860036990488994139314813986665699000071678
41534843695972182197917378267300625
Veky
la source
Je ne sais pas pourquoi cet vote négatif a voté contre mais je l'ai fait parce que cette réponse ne répond pas à la question. Le simple fait que quelque chose est correct n'implique en aucun cas que ce soit plus rapide ou plus lent. L'un est plus lent que l'autre car l'un peut travailler avec des types C tandis que l'autre doit travailler avec des objets Python.
Dimitris Fasarakis Hilliard
1
Merci pour l'explication. Eh bien, je pensais vraiment qu'il était évident qu'il était plus rapide de calculer juste l'approximation d'un nombre à environ 12 chiffres, que de les calculer tous exactement. Après tout, la seule raison pour laquelle nous utilisons des approximations est qu'elles sont plus rapides à calculer, non?
Veky