Чому деякі порівняння з плаваючими <цілими числами в чотири рази повільніше, ніж інші?


284

Порівнюючи поплавці з цілими числами, для деяких пар значень потрібно набагато більше часу, ніж інші значення аналогічної величини.

Наприклад:

>>> 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 більше бореться з деякими парами значень, ніж з іншими.


Це однаково і в 2.7, і в 3.x?
thefourtheye

Вищевказані терміни - від Python 3.4 - на моєму комп'ютері з Linux 2.7 працює аналогічна невідповідність часу (між 3 і 4-х-бітними часом повільніше).
Алекс Райлі

1
Дякуємо за цікаве написання. Мені цікаво, що надихнуло це питання - ви просто випадковим чином проводили порівняння чи за цим стоїть історія?
Ведрак

3
@Veedrac: Дякую Історії не так багато: я з роздумом цікавився, як швидко порівнюються плавці та цілі числа, приуротив кілька значень і помітив деякі невеликі відмінності. Тоді я зрозумів, що абсолютно не уявляю, як Python зумів точно порівняти поплавці та великі цілі числа. Я провів деякий час, намагаючись зрозуміти джерело і дізнався, що найгірше.
Алекс Райлі

2
@YvesDaoust: не ті конкретні цінності, ні (це було б неймовірною удачею!). Я спробував різні пари значень і помітив менші відмінності у таймінгах (наприклад, порівняння поплавця невеликої величини з аналогічними цілими числами та дуже великими цілими числами). Я дізнався про випадок 2 ^ 49 лише після перегляду джерела, щоб зрозуміти, як працює порівняння. Я вибрав значення у питанні, оскільки вони представили цю тему найбільш переконливо.
Алекс Райлі

Відповіді:


354

Коментар до вихідного коду Python для плаваючих об'єктів визнає, що:

Порівняння - це майже кошмар

Особливо це стосується порівняння поплавця з цілим числом, оскільки, на відміну від плавців, цілі числа в Python можуть бути довільно великими і завжди точними. Спроба передати ціле число на поплавок може втратити точність і зробити порівняння неточним. Спроба передати поплавок на ціле число не вийде, тому що будь-яка дробова частина буде втрачена.

Щоб подолати цю проблему, Python виконує серію перевірок, повертаючи результат, якщо один із перевірок успішний. Він порівнює знаки двох значень, потім, чи є ціле число "занадто великим", щоб бути поплавком, а потім порівнює показник поплавця з довжиною цілого числа. Якщо всі ці перевірки виявляються невдалими, для порівняння необхідно побудувати два нові об’єкти Python.

Порівнюючи поплавок vна ціле / довге 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 реалізації мови.


Порівняння більш детально

float_richcompareФункція обробляє порівняння між двома значеннями vі w.

Нижче покроковий опис перевірок, які виконує функція. Коментарі в джерелі Python насправді дуже корисні при спробі зрозуміти, що робить функція, тому я залишив їх там, де це доречно. Я також узагальнив ці перевірки у списку біля підніжжя відповіді.

Основна ідея полягає у відображенні об'єктів Python vта wдвох відповідних парних C, iі j, які потім можна легко порівняти, щоб дати правильний результат. І Python 2, і Python 3 використовують для цього однакові ідеї (колишні просто обробляють intі longвводять окремо).

Перше, що потрібно зробити, це перевірити, що v, безумовно, є поплавком Python, і відобразити його на C double i. Далі функція розглядає, чи wє також поплавком, і відображає його в C подвійному 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ці перевірки провалилися, це не поплавок Python. Тепер функція перевіряє, чи це ціле число Python. Якщо це так, найпростіший тест - це витяг знака vта знака w(повернути, 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 подвійне 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];
    }

Тепер функція дивиться на експонент поплавця. Нагадаємо, що поплавок може бути записаний (ігноруючи знак) як ознака * показник * 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|так як мантиса * 2 показник становить менше 2 Nbits .

Якщо не виконати ці дві перевірки, функція перевіряє, чи більше показника, ніж кількість бітів w. Це показує , що мантиса * 2 експонента більше 2 Nbits і так 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)(повернення помилкового).

    {
        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бути поплавком і відливати його як C подвійний. Тепер, якщо wтакож є поплавок:

  • Перевірте , чи правильно чи wце nanабо inf. Якщо так, обробляйте цей спеціальний чохол окремо, залежно від типу w.

  • Якщо ні, порівняйте vта wбезпосередньо їх представлення як C подвійне.

Якщо wце ціле число:

  • Витягнути ознаки vі w. Якщо вони різні, то ми знаємо vі wвідрізняємося, і чим більша цінність.

  • ( Знаки однакові. ) Перевірте, чи wє занадто багато бітів, щоб бути плавцем (більше, ніж size_t). Якщо це так, wмає велику величину , ніж v.

  • Перевірте, чи wмає 48 або менше біт. Якщо це так, його можна сміливо передати на подвійний C, не втрачаючи точності і порівняти з v.

  • ( wмає більше 48 біт. Тепер ми будемо розглядатись wяк додатне ціле число, змінивши опцію порівняння відповідно. )

  • Розглянемо показник поплавця v. Якщо показник від'ємний, то vменший 1і, отже, менший за будь-яке додатне ціле число. В іншому випадку, якщо показник менший за кількість бітів, wто він повинен бути меншим за w.

  • Якщо показник показника vбільше, ніж кількість бітів, wто vбільший за w.

  • ( Експонент такий самий, як кількість бітів в w. )

  • Заключна перевірка. Розділіть vна цілі та дробові частини. Подвійно цілу частину і додай 1 для компенсації дробової частини. Тепер подвоїмо ціле число w. Порівняйте замість цих двох нових цілих чисел, щоб отримати результат.


4
Молодці розробники Python - більшість мовних реалізацій просто вирішили проблему, сказавши, що порівняння float / integer не є точними.
користувач253751

4

Використовуючи 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

1
Я ще не використовував цю бібліотеку, але вона виглядає потенційно дуже корисною. Дякую!
Алекс Райлі

Її використовують sympy та mpmath
denfromufa

CPython також є decimalу стандартній бібліотеці
denfromufa
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.