Почему некоторые плавают



при сравнении поплавков с целыми числами некоторые пары значений оцениваются намного дольше, чем другие значения аналогичной величины.



например:



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


но если поплавок или целое число становится меньше или больше на определенную величину, сравнение выполняется гораздо быстрее:



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


смене оператора сравнения (например, с помощью == или > вместо этого) не влияет на время каким-либо заметным образом.



это не исключительно связано с величиной, потому что выбор больших или меньших значений может привести к более быстрым сравнениям, поэтому я подозреваю, что это связано с каким-то неудачным образом, когда биты выстраиваются в линию.



очевидно, что сравнение этих значений более чем достаточно быстро для большинства случаев использования. Мне просто любопытно, почему Python, похоже, больше борется с некоторыми парами значений, чем с другими.

681   2  

2 ответов:

комментарий в исходном коде Python для объектов float подтверждает, что:

сравнение в значительной степени кошмар

это особенно верно при сравнении поплавка с целым числом, потому что, в отличие от поплавков, целые числа в Python могут быть сколь угодно большими и всегда точными. Попытка привести целое число к поплавку может потерять точность и сделать сравнение неточным. Попытка привести поплавок к целому числу не является будет работать либо потому, что дробная часть будет потеряна.

чтобы обойти эту проблему, Python выполняет серию проверок, возвращая результат, если одна из проверок завершается успешно. Он сравнивает знаки двух значений, то ли целое число "слишком большое", чтобы быть поплавком, а затем сравнивает показатель степени поплавка с длиной целого числа. Если все эти проверки завершаются неудачей, необходимо построить два новых объекта Python для сравнения, чтобы получить результат.

при сравнении поплавок v к целому числу / long w в худшем случае это:

  • v и w имеют один и тот же знак (как положительный, так и отрицательный),
  • целое w имеет достаточно мало битов, что он может быть проведен в size_t тип (обычно 32 или 64 бит),
  • целое w имеет по крайней мере 49 бит,
  • показатель степени поплавка v - это то же, что и количество битов в w.

и это именно то, что мы имеем для значений в вопрос:

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

мы видим, что 49-это как показатель степени поплавка, так и количество битов в целом числе. Оба числа положительны, и поэтому четыре критерия выше выполнены.

выбор одного из значений, которое будет больше (или меньше), может изменить количество бит целого числа или значение экспоненты, и поэтому Python может определите результат сравнения без выполнения дорогостоящей окончательной проверки.

это специфично для реализации CPython языка.


сравнение более подробно

The float_richcompare функция обрабатывает сравнение между двумя значениями v и w.

Ниже приведено пошаговое описание проверок, которые выполняет функция. Комментарии в источнике Python являются на самом деле очень полезно, когда вы пытаетесь понять, что делает функция, поэтому я оставил их там, где это уместно. Я также суммировал эти проверки в списке внизу ответа.

основная идея заключается в отображении объектов Python v и w до двух соответствующих двойников C,i и j, который затем можно легко сравнить, чтобы дать правильный результат. И Python 2, и Python 3 используют для этого одни и те же идеи (первый просто обрабатывает int и long типы отдельно.)

первое, что нужно сделать, это проверить, что v определенно является поплавком Python и сопоставляет его с C double i. Следующая функция определяет, является ли w также является поплавком и отображает его на C double j. Это лучший сценарий для функции, так как все остальные проверки могут быть пропущены. Функция также проверяет, является ли v и inf или 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;
    }

теперь мы знаем, что если w не удалось эти проверки, это не a Python float. Теперь функция проверяет, является ли это целым числом Python. Если это так, то самый простой тест-извлечь знак v и w (return 0 если ноль, -1 если отрицательно, 1 если положительные). Если знаки разные, то это вся информация, необходимая для возврата результата сравнения:

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

если эта проверка не удалась, то v и w имеют тот же знак.

следующая проверка подсчитывает количество биты в целочисленном w. Если у него слишком много битов, то он не может удерживаться как поплавок и поэтому должен быть больше по величине, чем поплавок 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;
    }

с другой стороны, если число w имеет 48 или меньше бит, он может безопасно превращаться в C double j и по сравнению:

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

с этого момента, мы знаем, что w имеет 49 или более бит. Это будет удобно лечить w как положительное целое число, поэтому измените знак и оператор сравнения при необходимости:

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

теперь функция смотрит на экспоненту поплавка. Напомним, что поплавок может быть записан (игнорируя знак) как significand * 2показатель и что значение представляет собой число между 0,5 и 1:

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

проверяет две вещи. Если показатель меньше 0, то поплавок меньше 1 (и поэтому меньше по величине, чем любое целое число). Или, если показатель меньше числа битов в w у нас есть v < |w| так как significand * 2показатель меньше 2nbits.

при отсутствии этих двух проверок, функция смотрит, чтобы увидеть, является ли показатель больше, чем количество бит в w. Это показывает, что significand * 2показатель больше, чем 2nbits и v > |w|:

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

если эта проверка не удалась, мы знаем, что показатель поплавка v это то же самое, что и количество битов в целочисленном w.

единственный способ, которым эти два значения можно сравнить сейчас, это построить два новых целых числа Python из v и w. Идея в том, чтобы отбросить дробную часть v, двухместная целую часть, а затем добавить один. w также удваивается, и эти два новых объекта Python можно сравнить, чтобы дать правильное возвращаемое значение. Используя пример с малыми значениями,4.65 < 4 будет определяться сравнение (2*4)+1 == 9 < 8 == (2*4) (возврат false).

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

для краткости я опустил дополнительную проверку ошибок и отслеживание мусора Python должен делать, когда он создает эти новые объекты. Излишне говорить, что это добавляет дополнительные накладные расходы и объясняет, почему значения, выделенные в вопросе, значительно медленнее сравниваются, чем другие.


вот краткое описание проверок, выполняемых функцией сравнения.

пусть v будет поплавок и бросить его как с двойным. Теперь, если w также поплавок:

  • проверить, является ли w и nan или inf. Если да, то обрабатывайте этот особый случай отдельно в зависимости от типа w.

  • если нет, то сравниваем v и w непосредственно по их представлениям как C удваивается.

если w - это целое число:

  • извлечение признаков v и w. Если они разные, то мы знаем!--9--> и w отличаются и большей стоимостью.

  • (знаки те же.) проверяет, является ли w имеет слишком много битов, чтобы быть поплавком (более size_t). Если так, то w имеет большую величину, чем v.

  • проверить, если w есть 48 или меньше бит. Если это так, то его можно безопасно привести к двойнику C без потери точности и по сравнению с v.

  • (w более 48 бит. Теперь будем лечить w как положительное целое число, изменив сравнение op соответствующим образом.)

  • рассмотрим показатель степени поплавка v. Если показатель отрицательный, то v меньше 1 и, следовательно, меньше, чем любое положительное целое число. Иначе, если показатель меньше числа битов в w тогда это должно быть меньше чем w.

  • если показатель v больше, чем количество битов в w затем v больше w.

  • (показатель равен количеству битов в w.)

  • окончательная проверка. Сплит v в его целые и дробные части. Удвоить целочисленную часть и добавить 1 для компенсации дробной части. Теперь удвойте целое число w. Сравните эти два новых целых числа вместо того, чтобы получить результат.

используя gmpy2 с произвольной точностью поплавков и целых чисел можно получить более равномерное сравнение производительности:

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

Comments

    Ничего не найдено.