Является ли функция math.ulp более точной, чем явная формула в Python?

В арифметике с плавающей запятой - единица, стоящая на последнем месте (ULP) числа с плавающей запятой. число — это расстояние между этим числом и последующим, т. е. значение его младшей значащей цифры (крайней правой цифры), если оно равно 1. Оно определяется по следующей формуле:

ULP(x) = b−(p−1) • |x |

где b — основание (2 для двоичных чисел), а p (53 для мантиссы двойной точности) — точность.

В Python 3.9 появилась новая функция math.ulp для вычисления ULP число с плавающей запятой.

С помощью этой функции предыдущая формула проверяется, как и ожидалось, для ULP, равного 1:

>>> math.ulp(1)
2.220446049250313e-16
>>> 2**(-(53 - 1)) * abs(1)
2.220446049250313e-16

но это не проверено для ULP 10−10, например:

>>> math.ulp(1e-10)
1.2924697071141057e-26
>>> 2**(-(53 - 1)) * abs(1e-10)
2.2204460492503132e-26

Является ли math.ulp(x) более точным, чем 2**(-(53 - 1)) * abs(x)? Почему?

Реализация CPython находится в Modules/mathmodule.c. #L3408-L3427, но я не могу найти реализацию вызываемой функции nextafter, чтобы понять:

static double
math_ulp_impl(PyObject *module, double x)
/*[clinic end generated code: output=f5207867a9384dd4 input=31f9bfbbe373fcaa]*/
{
    if (Py_IS_NAN(x)) {
        return x;
    }
    x = fabs(x);
    if (Py_IS_INFINITY(x)) {
        return x;
    }
    double inf = m_inf();
    double x2 = nextafter(x, inf);
    if (Py_IS_INFINITY(x2)) {
        /* special case: x is the largest positive representable float */
        x2 = nextafter(x, -inf);
        return x - x2;
    }
    return x2 - x;
}

person Maggyero    schedule 03.02.2021    source источник
comment
nextafter — это функция из стандартной библиотеки C. Я ожидаю, что встроенный ulp будет более надежным просто потому, что он... ну, проще и придерживается вычитания, в то время как явная формула выполняет некоторые причудливые математические вычисления (возведение в степень) прямо на грани возможностей именно то, что он пытается исследовать. Но я не анализировал это подробно, так что это комментарий, а не ответ. :)   -  person Ture Pålsson    schedule 03.02.2021
comment
К вашему сведению, «пробел между этим номером и последующим номером» не является правильным описанием ULP. Расстояние от -2 до следующего представимого числа, -1,9999 с чем-то, составляет половину ULP от -2.   -  person Eric Postpischil    schedule 03.02.2021
comment
@EricPostpischil Спасибо, вы абсолютно правы, согласно Википедии, ULP( x) = 2^(-(53 - 1)) * 2^exponent(x), где exponent(x) — это IEEE 754 нормализованный показатель степени x, т. е. math.ulp(x) == 2**(-(53 - 1)) * 2**math.floor(math.log2(x)). Не могли бы вы написать ответ, чтобы я мог его принять?   -  person Maggyero    schedule 04.02.2021


Ответы (1)


2−(53−1) • |x| (или 2**(-(53 - 1)) * abs(x)) не является формулой для ULP(x) (или math.ulp(x)), поскольку она не дает значение 1 в позиции младшего бита x а скорее значение мантиссы x (1.something), масштабированное до положения самого младшего бита x. Если x не является степенью двойки, его значение превышает 1, и формула слишком велика.

правильная формула: 2−(53−1) • 2max(e, −1022), где e — это IEEE 754 нормализованный показатель степени для x, т. е. 2e ≤ |< эм>х| ‹ 2e+1 (или 2**(-(53 - 1)) * 2**max(math.floor(math.log2(x)), -1022)).

person Eric Postpischil    schedule 03.02.2021