Как правильно сравнить целое число и значение с плавающей точкой? - PullRequest
7 голосов
/ 06 ноября 2019

Как сравнить целое число и значение с плавающей точкой правильный путь ™ ?

Встроенные операторы сравнения дают неправильные результаты в некоторых случаях, например:

#include <iomanip>
#include <iostream>

int main()
{
    long long a = 999999984306749439;
    float     b = 999999984306749440.f; // This number can be represented exactly by a `float`.

    std::cout << std::setprecision(1000);
    std::cout << a << " < " << b << " = " << (a < b) << '\n';
    // Prints `999999984306749439 < 999999984306749440 = 0`, but it should be `1`.
}

Очевидно, что операторы сравнения преобразуют оба операнда в один и тот же тип, прежде чем сравнивать их. Здесь lhs преобразуется в float, что приводит к потере точности и приводит к неверному результату.

Несмотря на то, что я понимаю, что происходит, я не уверен, как обойти эту проблему.


Отказ от ответственности: в примере используются float и long long, но я ищу универсальное решение, которое работает для каждой комбинации целочисленного типа и типа с плавающей запятой.

Ответы [ 6 ]

4 голосов
/ 06 ноября 2019

(Ограничение этого ответа положительными числами; обобщение тривиально.)

  1. Получите число бит в вашем показателе степени для float на вашей платформе вместе с основанием. Если у вас IEEE754 32-битный float, то это тривиальный шаг.

  2. Используйте (1) для вычисления наибольшего нецелого значения, которое может быть сохранено в вашем float,std::numeric_limits не определяет это значение, досадно, поэтому вам нужно сделать это самостоятельно. Для 32-битного IEEE754 вы можете выбрать простой вариант: 8388607.5 - это самый большой нецелый тип float.

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

  4. На этом этапе float является целым числом. Проверьте, находится ли он в пределах вашего long long. Если он выходит за пределы диапазона, то известен результат <.

  5. Если вы зайдете так далеко, вы можете смело бросить float в long long и сделатьсравнение.

3 голосов
/ 06 ноября 2019

Вот что у меня получилось.

Кредит за алгоритм идет на @chux;его подход, кажется, превосходит другие предложения. Вы можете найти несколько альтернативных реализаций в истории редактирования.

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

#include <cmath>
#include <limits>
#include <type_traits>

enum partial_ordering {less, equal, greater, unordered};

template <typename I, typename F>
partial_ordering compare_int_float(I i, F f)
{
    if constexpr (std::is_integral_v<F> && std::is_floating_point_v<I>)
    {
        return compare_int_float(f, i);
    }
    else
    {
        static_assert(std::is_integral_v<I> && std::is_floating_point_v<F>);
        static_assert(std::numeric_limits<F>::radix == 2);

        // This should be exactly representable as F due to being a power of two.
        constexpr F I_min_as_F = std::numeric_limits<I>::min();

        // The `numeric_limits<I>::max()` itself might not be representable as F, so we use this instead.
        constexpr F I_max_as_F_plus_1 = F(std::numeric_limits<I>::max()/2+1) * 2;

        // Check if the constants above overflowed to infinity. Normally this shouldn't happen.
        constexpr bool limits_overflow = I_min_as_F * 2 == I_min_as_F || I_max_as_F_plus_1 * 2 == I_max_as_F_plus_1;
        if constexpr (limits_overflow)
        {
            // Manually check for special floating-point values.
            if (std::isinf(f))
                return f > 0 ? less : greater;
            if (std::isnan(f))
                return unordered;
        }

        if (limits_overflow || f >= I_min_as_F)
        {
            // `f <= I_max_as_F_plus_1 - 1` would be problematic due to rounding, so we use this instead.
            if (limits_overflow || f - I_max_as_F_plus_1 <= -1)
            {
                I f_trunc = f;
                if (f_trunc < i)
                    return greater;
                if (f_trunc > i)
                    return less;

                F f_frac = f - f_trunc;
                if (f_frac < 0)
                    return greater;
                if (f_frac > 0)
                    return less;

                return equal;
            }

            return less;
        }

        if (f < 0)
            return greater;

        return unordered;
    }
}

Если вы хотите поэкспериментировать с этим, вот несколькоконтрольные примеры:

#include <cmath>
#include <iomanip>
#include <iostream> 

void compare_print(long long a, float b, int n = 0)
{
    if (n == 0)
    {
        auto result = compare_int_float(a,b);
        std::cout << a << ' ' << "<=>?"[int(result)] << ' ' << b << '\n';
    }
    else
    {
        for (int i = 0; i < n; i++)
            b = std::nextafter(b, -INFINITY);

        for (int i = 0; i <= n*2; i++)
        {
            compare_print(a, b);
            b = std::nextafter(b, INFINITY);
        }

        std::cout << '\n';
    }
}

int main()
{    
    std::cout << std::setprecision(1000);

    compare_print(999999984306749440,
                  999999984306749440.f, 2);

    compare_print(999999984306749439,
                  999999984306749440.f, 2);

    compare_print(100,
                  100.f, 2);

    compare_print(-100,
                  -100.f, 2);

    compare_print(0,
                  0.f, 2);

    compare_print((long long)0x8000'0000'0000'0000,
                  (long long)0x8000'0000'0000'0000, 2);

    compare_print(42, INFINITY);
    compare_print(42, -INFINITY);
    compare_print(42, NAN);
    std::cout << '\n';

    compare_print(1388608,
                  1388608.f, 2);

    compare_print(12388608,
                  12388608.f, 2);
}

(запустите код)

2 голосов
/ 10 ноября 2019

Приведенный ниже код работает с целочисленными типами данных не более 64 бит и типами данных с плавающей запятой не более чем с точностью до ieee-754. Для более широких типов данных можно использовать ту же идею, но вам придется адаптировать код. Поскольку я не очень знаком с C ++, код написан на C. Не должно быть слишком сложно преобразовать его в код в стиле C ++. Код не имеет ответвлений, что может повысить производительность.

#include <stdio.h>
// gcc -O3 -march=haswell cmp.c
// Assume long long int is 64 bits.
// Assume ieee-754 double precision.
int long_long_less_than_double(long long int i, double y) {
    long long i_lo = i & 0x00000000FFFFFFFF;   // Extract lower 32 bits.
    long long i_hi = i & 0xFFFFFFFF00000000;   // Extract upper 32 bits.
    double x_lo = (double)i_lo;                // Exact conversion to double, no rounding errors!
    double x_hi = (double)i_hi;                // 
    return ( x_lo < (y - x_hi) );              // If i is close to y then y - x_hi is exact,
                                               // due to Sterbenz' lemma.
    // i < y
    // i_lo +i_hi < y      
    // i_lo < (y - i_hi)
    // x_lo < (y - x_hi)
}

int long_long_equals_double(long long int i, double y) {
    long long i_lo = i & 0x00000000FFFFFFFF;   
    long long i_hi = i & 0xFFFFFFFF00000000;   
    double x_lo = (double)i_lo;                    
    double x_hi = (double)i_hi;                    
    return ( x_lo == (y - x_hi) );                  
}                                                  


int main()
{
    long long a0 = 999999984306749439;
    long long a1 = 999999984306749440;    // Hex number: 0x0DE0B6B000000000
    long long a2 = 999999984306749441;
    float     b = 999999984306749440.f;   // This number can be represented exactly by a `float`.

    printf("%lli less_than %20.1f = %i\n", a0, b, long_long_less_than_double(a0, b));  // Implicit conversion from float to double
    printf("%lli less_than %20.1f = %i\n", a1, b, long_long_less_than_double(a1, b));

    printf("%lli equals    %20.1f = %i\n", a0, b, long_long_equals_double(a0, b));
    printf("%lli equals    %20.1f = %i\n", a1, b, long_long_equals_double(a1, b));
    printf("%lli equals    %20.1f = %i\n\n", a2, b, long_long_equals_double(a2, b));


    long long c0 = 1311693406324658687;
    long long c1 = 1311693406324658688;   // Hex number: 0x1234123412341200
    long long c2 = 1311693406324658689; 
    double     d = 1311693406324658688.0; // This number can be represented exactly by a `double`.

    printf("%lli less_than %20.1f = %i\n", c0, d, long_long_less_than_double(c0, d));
    printf("%lli less_than %20.1f = %i\n", c1, d, long_long_less_than_double(c1, d));

    printf("%lli equals    %20.1f = %i\n", c0, d, long_long_equals_double(c0, d));
    printf("%lli equals    %20.1f = %i\n", c1, d, long_long_equals_double(c1, d));
    printf("%lli equals    %20.1f = %i\n", c2, d, long_long_equals_double(c2, d));


    return 0;
}

Идея состоит в том, чтобы разбить 64-битное целое число i на 32 старших бита i_hi и 32 младших бита i_lo, которые преобразуются в двойные числа x_hi и x_lo без каких-либоошибки округления. Если double y близко к x_hi, то вычитание с плавающей запятой y - x_hi является точным из-за леммы Sterbenz . Таким образом, вместо x_lo + x_hi < y мы можем проверить x_lo < (y - x_hi), что более точно! Если double y не близко к x_hi, то y - x_hi является неточным, но в этом случае нам не нужна точность, потому что тогда |y - x_hi| намного больше, чем |x_lo|. Другими словами: если i и y сильно отличаются, нам не нужно беспокоиться о значении младших 32 бит.

Вывод:

    999999984306749439 less_than 999999984306749440.0 = 1
    999999984306749440 less_than 999999984306749440.0 = 0
    999999984306749439 equals    999999984306749440.0 = 0
    999999984306749440 equals    999999984306749440.0 = 1
    999999984306749441 equals    999999984306749440.0 = 0

    1311693406324658687 less_than 1311693406324658688.0 = 1
    1311693406324658688 less_than 1311693406324658688.0 = 0
    1311693406324658687 equals    1311693406324658688.0 = 0
    1311693406324658688 equals    1311693406324658688.0 = 1
    1311693406324658689 equals    1311693406324658688.0 = 0
2 голосов
/ 08 ноября 2019

Для сравнения FP f и целое число i на равенство:

(код является репрезентативным и использует сравнение float и long long в качестве примера)

  1. Если f является NaN, бесконечностью или имеет дробную часть (возможно, использовать frexp()), f не равно i.

    float ipart;
    // C++
    if (frexp(f, &ipart) != 0) return not_equal;
    // C
    if (frexpf(f, &ipart) != 0) return not_equal;
    
  2. Преобразование числовых пределов i в точно представимые значения FP (степени 2) вблизи этих пределов. ** Легко сделать, еслимы предполагаем, что FP - это не редкая кодировка base 10, а диапазон double превышает диапазон на i. Воспользуйтесь преимуществом, что целочисленные пределы величин будут или близки к Число Мерсенна . (Извините, пример кода C-ish)

    #define FP_INT_MAX_PLUS1 ((LLONG_MAX/2 + 1)*2.0)
    #define FP_INT_MIN (LLONG_MIN*1.0)
    
  3. Сравнить f с ограничениями

    if (f >= FP_INT_MAX_PLUS1) return not_equal;
    if (f < FP_INT_MIN) return not_equal;
    
  4. Преобразовать fцелое число и сравнение

    return (long long) f == i;
    

Для сравнения FP f и целое число i для <, >, == или не сопоставимо:

(с использованием вышеуказанных пределов)

  1. Тест f >= lower limit

    if (f >= FP_INT_MIN) {
    
  2. Тест f <= upper limit

      // reform below to cope with effects of rounding
      // if (f <= FP_INT_MAX_PLUS1 - 1)
      if (f - FP_INT_MAX_PLUS1 <= -1.0) {
    
  3. Преобразовать f в целое число / дробь и сравнить

        // at this point `f` is in the range of `i`
        long long ipart = (long long) f;
        if (ipart < i) return f_less_than_i;
        if (ipart > i) return f_more_than_i;
    
        float frac = f - ipart;
        if (frac < 0) return f_less_than_i;
        if (frac > 0) return f_more_than_i;
        return equal;
      }
    
  4. Обрабатывать крайние случаи

      else return f_more_than_i;
    }
    if (f < 0.0) return f_less_than_i;
    return not_comparable;
    

Возможны упрощения, но я хотел передать алгоритм.


** Дополнительный условный код, необходимый для работы с целочисленным кодированием, не равным 2. ,Это очень похоже на код MAX.

1 голос
/ 07 ноября 2019

Вот как я решил это недавно в opensmalltalk VM для сравнения ограниченных целых чисел:

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

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

В OpenSmalltalk VM это не проблема, поскольку SmallInteger использует не более 61 бита, поэтому я не сталпопытаться ее решить.

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

Как сравнить точное значение SmallInteger и Float в Smalltalk

Для неограниченных (произвольно больших) целых чисел сравнение выполняется в Integer, но есть несколько приемов для ускорения сравнения. Это делается не в виртуальной машине, а в коде Smalltalk (хороший пример - Squeak).

0 голосов
/ 06 ноября 2019

Используйте double, а не float. Возьмите двойное значение + 0,5. Обрежьте его статическим приведением к длинному. Теперь сравните два длинных длинных.

...