web-dev-qa-db-fra.com

Pourquoi certaines comparaisons flottantes <nombres entiers sont-elles quatre fois plus lentes que d'autres?

Lors de la comparaison de flottants à des entiers, l'évaluation de certaines paires de valeurs prend beaucoup plus de temps que d'autres valeurs d'une magnitude similaire.

Par exemple:

>>> import timeit
>>> timeit.timeit("562949953420000.7 < 562949953421000") # run 1 million times
0.5387085462592742

Mais si le flottant ou l'entier est réduit ou agrandi d'un certain montant, la comparaison est beaucoup plus rapide:

>>> timeit.timeit("562949953420000.7 < 562949953422000") # integer increased by 1000
0.1481498428446173
>>> timeit.timeit("562949953423001.8 < 562949953421000") # float increased by 3001.1
0.1459577925548956

Modification de l’opérateur de comparaison (par exemple, en utilisant == ou > à la place) n’affecte pas les temps de manière notable.

Ce n'est pas niquement lié à la magnitude, car choisir des valeurs plus grandes ou plus petites peut entraîner des comparaisons plus rapides. Je suppose donc que les trames s'alignent de manière malheureuse.

Il est clair que la comparaison de ces valeurs est plus que rapide pour la plupart des cas d'utilisation. Je suis simplement curieux de savoir pourquoi Python semble lutter davantage avec certaines paires de valeurs que d’autres.

282
Alex Riley

Un commentaire dans le code source Python pour les objets flottants) reconnaît que:

La comparaison est à peu près un cauchemar

C’est particulièrement vrai lorsqu’on compare un float à un entier, car contrairement à float, les entiers dans Python peuvent être arbitrairement grands et sont toujours exacts. Tenter de convertir cet entier en float risque de perdre de la précision et essayer de convertir le flottant en un entier ne va pas marcher non plus, car toute partie décimale sera perdue.

Pour contourner ce problème, Python effectue une série de vérifications et renvoie le résultat si l'une des vérifications réussit. Il compare les signes des deux valeurs, puis détermine si l'entier est "trop ​​grand" pour être un float, compare l’exposant du float à la longueur de l’entier Si tous ces contrôles échouent, il est nécessaire de construire deux nouveaux Python à comparer afin d’obtenir le résultat.

Lorsque l'on compare un float v à un entier/long w, le pire des cas est celui-ci:

  • v et w ont le même signe (tous les deux positifs ou négatifs),
  • le nombre entier w contient assez de bits pour pouvoir être conservé dans le size_t type (généralement 32 ou 64 bits),
  • l'entier w a au moins 49 bits,
  • l'exposant du float v est identique au nombre de bits dans w.

Et c'est exactement ce que nous avons pour les valeurs de la question:

>>> import math
>>> math.frexp(562949953420000.7) # gives the float's (significand, exponent) pair
(0.9999999999976706, 49)
>>> (562949953421000).bit_length()
49

Nous voyons que 49 est à la fois l'exposant du float et le nombre de bits de l'entier. Les deux chiffres étant positifs, les quatre critères ci-dessus sont remplis.

Choisir l'une des valeurs comme étant plus grande (ou plus petite) peut changer le nombre de bits de l'entier ou la valeur de l'exposant, et ainsi Python peut déterminer le résultat de la comparaison sans effectuer la vérification finale coûteuse.

Ceci est spécifique à l'implémentation CPython du langage.


La comparaison plus en détail

Le float_richcompare La fonction gère la comparaison entre deux valeurs v et w.

Vous trouverez ci-dessous une description étape par étape des vérifications effectuées par la fonction. Les commentaires dans la source Python sont en fait très utiles pour essayer de comprendre ce que fait la fonction, je les ai donc laissés là où ils étaient pertinents. J'ai également résumé ces vérifications dans une liste à la pied de la réponse.

L'idée principale est de mapper les objets Python v et w] vers deux doubles C appropriés, i et j , qui peuvent ensuite être facilement comparés pour donner le résultat correct. Python 2 et Python 3 utilisent les mêmes idées pour ce faire (le premier traite simplement int et les types long séparément).

La première chose à faire est de vérifier que v est bien un Python float et le mapper sur un double C i. La fonction vérifie ensuite si w est également un float et le mappe sur un double C j. C'est le meilleur scénario pour la fonction, car toutes les autres vérifications peuvent être ignorées. La fonction vérifie également si v est inf ou nan:

static PyObject*
float_richcompare(PyObject *v, PyObject *w, int op)
{
    double i, j;
    int r = 0;
    assert(PyFloat_Check(v));       
    i = PyFloat_AS_DOUBLE(v);       

    if (PyFloat_Check(w))           
        j = PyFloat_AS_DOUBLE(w);   

    else if (!Py_IS_FINITE(i)) {
        if (PyLong_Check(w))
            j = 0.0;
        else
            goto Unimplemented;
    }

Nous savons maintenant que si w échouait à ces contrôles, il ne s'agissait pas d'un Python float. Maintenant, la fonction vérifie s'il s'agit d'un Python entier. Si tel est le cas, le test le plus simple consiste à extraire le signe de v et le signe de w (return 0 si zéro, -1 si négatif, 1 si positif). Si les signes sont différents, voici toutes les informations nécessaires pour renvoyer le résultat de la comparaison:

    else if (PyLong_Check(w)) {
        int vsign = i == 0.0 ? 0 : i < 0.0 ? -1 : 1;
        int wsign = _PyLong_Sign(w);
        size_t nbits;
        int exponent;

        if (vsign != wsign) {
            /* Magnitudes are irrelevant -- the signs alone
             * determine the outcome.
             */
            i = (double)vsign;
            j = (double)wsign;
            goto Compare;
        }
    }   

Si cette vérification échoue, alors v et w ont le même signe.

La vérification suivante compte le nombre de bits dans l'entier w. S'il contient trop de bits, il ne peut pas être tenu comme un flottant et doit donc être d'une magnitude supérieure au flottant v:

    nbits = _PyLong_NumBits(w);
    if (nbits == (size_t)-1 && PyErr_Occurred()) {
        /* This long is so large that size_t isn't big enough
         * to hold the # of bits.  Replace with little doubles
         * that give the same outcome -- w is so large that
         * its magnitude must exceed the magnitude of any
         * finite float.
         */
        PyErr_Clear();
        i = (double)vsign;
        assert(wsign != 0);
        j = wsign * 2.0;
        goto Compare;
    }

Par contre, si l’entier w a 48 bits ou moins, il peut en toute sécurité activer un double C j et le comparer:

    if (nbits <= 48) {
        j = PyLong_AsDouble(w);
        /* It's impossible that <= 48 bits overflowed. */
        assert(j != -1.0 || ! PyErr_Occurred());
        goto Compare;
    }

À partir de ce moment, nous savons que w a 49 bits ou plus. Il sera commode de traiter w comme un entier positif, changez donc le signe et l'opérateur de comparaison si nécessaire:

    if (nbits <= 48) {
        /* "Multiply both sides" by -1; this also swaps the
         * comparator.
         */
        i = -i;
        op = _Py_SwappedOp[op];
    }

La fonction regarde maintenant l'exposant du float. Rappelez-vous qu'un float peut être écrit (en ignorant le signe) de manière significative * 2exposant et que le significand représente un nombre compris entre 0,5 et 1:

    (void) frexp(i, &exponent);
    if (exponent < 0 || (size_t)exponent < nbits) {
        i = 1.0;
        j = 2.0;
        goto Compare;
    }

Cela vérifie deux choses. Si l'exposant est inférieur à 0, le flottant est inférieur à 1 (et donc de magnitude inférieure à celle de tout nombre entier). Ou, si l'exposant est inférieur au nombre de bits dans w, alors nous avons ce v < |w| depuis significand * 2exposant est inférieur à 2nbits.

À défaut de ces deux vérifications, la fonction recherche si l'exposant est supérieur au nombre de bits dans w. Cela montre que significand * 2exposant est supérieur à 2nbits et donc v > |w|:

    if ((size_t)exponent > nbits) {
        i = 2.0;
        j = 1.0;
        goto Compare;
    }

Si cette vérification échoue, nous savons que l'exposant du flottant v est identique au nombre de bits de l'entier w.

La seule façon de comparer les deux valeurs maintenant consiste à construire deux nouveaux Python entiers à partir de v et de w. L'idée est de supprimer la partie décimale. de v, doublez la partie entière, puis ajoutez-en un. w est également doublé et ces deux nouveaux Python peuvent être comparés pour obtenir le retour correct En utilisant un exemple avec de petites valeurs, 4.65 < 4 _ serait déterminé par la comparaison (2*4)+1 == 9 < 8 == (2*4) (retourne faux).

    {
        double fracpart;
        double intpart;
        PyObject *result = NULL;
        PyObject *one = NULL;
        PyObject *vv = NULL;
        PyObject *ww = w;

        // snip

        fracpart = modf(i, &intpart); // split i (the double that v mapped to)
        vv = PyLong_FromDouble(intpart);

        // snip

        if (fracpart != 0.0) {
            /* Shift left, and or a 1 bit into vv
             * to represent the lost fraction.
             */
            PyObject *temp;

            one = PyLong_FromLong(1);

            temp = PyNumber_Lshift(ww, one); // left-shift doubles an integer
            ww = temp;

            temp = PyNumber_Lshift(vv, one);
            vv = temp;

            temp = PyNumber_Or(vv, one); // a doubled integer is even, so this adds 1
            vv = temp;
        }
        // snip
    }
}

Par souci de brièveté, j’ai oublié la vérification supplémentaire des erreurs et le suivi des ordures Python doit faire quand il crée ces nouveaux objets. Inutile de dire que cela ajoute une surcharge supplémentaire et explique pourquoi les valeurs mises en surbrillance dans la question sont nettement plus lents à comparer que les autres.


Voici un résumé des contrôles effectués par la fonction de comparaison.

Soit v un float et transformez-le en C double. Maintenant, si w est aussi un float:

  • Vérifiez si w est nan ou inf. Si tel est le cas, traitez ce cas spécial séparément en fonction du type de w.

  • Sinon, comparez directement v et w par leurs représentations car C double.

Si w est un entier:

  • Extrayez les signes de v et w. S'ils sont différents, nous savons que v et w sont différents et quelle est la plus grande valeur.

  • ( Les signes sont les mêmes. ) Vérifiez si w a trop de bits pour être un float (plus que size_t). Si tel est le cas, la valeur de w est supérieure à celle de v.

  • Vérifiez si w a 48 bits ou moins. Si tel est le cas, il peut être converti en C double sans perdre sa précision et comparé à v.

  • (w a plus de 48 bits. Nous allons maintenant traiter w comme un entier positif ayant changé l'opérateur de comparaison de manière appropriée. )

  • Considérons l'exposant du float v. Si l'exposant est négatif, alors v est inférieur à 1 et donc inférieur à tout entier positif. Sinon, si l'exposant est inférieur au nombre de bits dans w, il doit alors être inférieur à w.

  • Si l'exposant de v est supérieur au nombre de bits dans w, alors v est supérieur à w.

  • ( L'exposant est identique au nombre de bits dans w. )

  • Le dernier contrôle. Divisez v en ses parties entières et fractionnaires. Doublez la partie entière et ajoutez 1 pour compenser la partie fractionnaire. Maintenant, doublez le nombre entier w. Comparez ces deux nouveaux entiers à la place pour obtenir le résultat.

351
Alex Riley

En utilisant gmpy2 avec une précision arbitraire float et integers, il est possible d’obtenir des performances de comparaison plus uniformes:

~ $ ptipython
Python 3.5.1 |Anaconda 4.0.0 (64-bit)| (default, Dec  7 2015, 11:16:01) 
Type "copyright", "credits" or "license" for more information.

IPython 4.1.2 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: import gmpy2

In [2]: from gmpy2 import mpfr

In [3]: from gmpy2 import mpz

In [4]: gmpy2.get_context().precision=200

In [5]: i1=562949953421000

In [6]: i2=562949953422000

In [7]: f=562949953420000.7

In [8]: i11=mpz('562949953421000')

In [9]: i12=mpz('562949953422000')

In [10]: f1=mpfr('562949953420000.7')

In [11]: f<i1
Out[11]: True

In [12]: f<i2
Out[12]: True

In [13]: f1<i11
Out[13]: True

In [14]: f1<i12
Out[14]: True

In [15]: %timeit f<i1
The slowest run took 10.15 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 441 ns per loop

In [16]: %timeit f<i2
The slowest run took 12.55 times longer than the fastest. This could mean that an intermediate result is being cached.
10000000 loops, best of 3: 152 ns per loop

In [17]: %timeit f1<i11
The slowest run took 32.04 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 269 ns per loop

In [18]: %timeit f1<i12
The slowest run took 36.81 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 231 ns per loop

In [19]: %timeit f<i11
The slowest run took 78.26 times longer than the fastest. This could mean that an intermediate result is being cached.
10000000 loops, best of 3: 156 ns per loop

In [20]: %timeit f<i12
The slowest run took 21.24 times longer than the fastest. This could mean that an intermediate result is being cached.
10000000 loops, best of 3: 194 ns per loop

In [21]: %timeit f1<i1
The slowest run took 37.61 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 275 ns per loop

In [22]: %timeit f1<i2
The slowest run took 39.03 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 259 ns per loop
4
denfromufa