Комментарий в исходном коде 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 на самом деле очень полезны при попытке понять, что делает функция, поэтому я оставил их там, где это уместно. Я также суммировал эти проверки в списке внизу ответа.
Основная идея состоит в том, чтобы сопоставить объекты Питона v
и w
два соответствующих двойников C, i
и j
, которые затем можно легко сравнить , чтобы дать правильный результат. И Python 2, и Python 3 используют для этого одни и те же идеи (первый только обрабатывает int
и long
печатает отдельно).
Первое, что нужно сделать, это проверить, что v
это определенно число с плавающей точкой Python, и отобразить его на C double i
. Затем функция проверяет, w
является ли также число с плавающей запятой, и сопоставляет его с двойным Си 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 или меньше битов, оно может безопасно превратиться в двойное число Си 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 нбит .
Если эти две проверки не пройдены, функция проверяет, больше ли показатель степени, чем число бит в 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)
(возвращает 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
быть плавающим и бросить его как 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
. Вместо этого сравните эти два новых целых числа, чтобы получить результат.