Почему некоторые плавают
при сравнении поплавков с целыми числами некоторые пары значений оцениваются намного дольше, чем другие значения аналогичной величины.
например:
>>> 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 ответов:
комментарий в исходном коде Python для объектов float подтверждает, что:
это особенно верно при сравнении поплавка с целым числом, потому что, в отличие от поплавков, целые числа в Python могут быть сколь угодно большими и всегда точными. Попытка привести целое число к поплавку может потерять точность и сделать сравнение неточным. Попытка привести поплавок к целому числу не является будет работать либо потому, что дробная часть будет потеряна.
чтобы обойти эту проблему, Python выполняет серию проверок, возвращая результат, если одна из проверок завершается успешно. Он сравнивает знаки двух значений, то ли целое число "слишком большое", чтобы быть поплавком, а затем сравнивает показатель степени поплавка с длиной целого числа. Если все эти проверки завершаются неудачей, необходимо построить два новых объекта Python для сравнения, чтобы получить результат.
при сравнении поплавок
vк целому числу / longwв худшем случае это:
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 doublei. Следующая функция определяет, является лиwтакже является поплавком и отображает его на C doublej. Это лучший сценарий для функции, так как все остальные проверки могут быть пропущены. Функция также проверяет, является ли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(return0если ноль,-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 doublejи по сравнению: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