Всегда ли приведение std :: floor () и std :: ceil () к целочисленному типу дает правильный результат?

Я параною, что одна из этих функций может дать неверный результат, например:

std::floor(2000.0 / 1000.0) --> std::floor(1.999999999999) --> 1
or
std::ceil(18 / 3) --> std::ceil(6.000000000001) --> 7

Может ли такое случиться? Если такой риск действительно существует, я планирую использовать перечисленные ниже функции для безопасной работы. Но действительно ли это необходимо?

constexpr long double EPSILON = 1e-10;

intmax_t GuaranteedFloor(const long double & Number)
{
    if (Number > 0)
    {
        return static_cast<intmax_t>(std::floor(Number) + EPSILON);
    }
    else
    {
        return static_cast<intmax_t>(std::floor(Number) - EPSILON);
    }
}

intmax_t GuaranteedCeil(const long double & Number)
{
    if (Number > 0)
    {
        return static_cast<intmax_t>(std::ceil(Number) + EPSILON);
    }
    else
    {
        return static_cast<intmax_t>(std::ceil(Number) - EPSILON);
    }
}

(Примечание: я предполагаю, что данный аргумент 'long double' будет соответствовать типу возвращаемого значения 'intmax_t'.)


person hkBattousai    schedule 25.01.2016    source источник
comment
Я бы подумал, что эти примеры будут безопасными (целые числа с точностью до плавающей точки), но, например, 3.3 / 1.1 может дать не точно 3.   -  person TripeHound    schedule 25.01.2016
comment
EPSILON тебя не спасет. Это наименьшая значимая разница при 1,0, то есть наименьшее значение, которое вы можете добавить к 1,0, чтобы получить другое значение. Если ваш результат может быть больше или меньше 1.0, вам понадобится другой EPSILON. Если вы думаете, что вам нужно EPSILON для чего-нибудь, скорее всего, вы собираетесь внести в свое программное обеспечение очень тонкую ошибку.   -  person DevSolar    schedule 25.01.2016
comment
Вместо static_cast вы можете рассмотреть std::lround, который возвращает ближайшее целое число: int i = std::lround(std::ceil(f)). Это избавит от необходимости использовать эпсилон или условные выражения.   -  person Jonathan Lidbeck    schedule 20.07.2018


Ответы (4)


У людей часто создается впечатление, что операции с плавающей запятой дают результаты с небольшими, непредсказуемыми, квазислучайными ошибками. Это впечатление неверно.

Арифметические вычисления с плавающей запятой выполняются максимально точными. 18/3 всегда будет давать ровно 6. Результат 1/3 не будет ровно одной третью, но будет ближайшим к одной трети числом, которое может быть представлено как число с плавающей запятой.

Так что показанные вами примеры гарантированно работают всегда. Что касается предложенного вами гарантированного пола / потолка, это плохая идея. Определенные последовательности операций могут легко вызвать ошибку намного выше 1e-10, а некоторые другие варианты использования потребуют, чтобы 1e-10 был правильно распознан (и зафиксирован) как ненулевой.

Как показывает практика, жестко запрограммированные значения epsilon - это ошибки в вашем коде.

person Sneftel    schedule 25.01.2016

В конкретных примерах, которые вы перечисляете, я не думаю, что эти ошибки когда-либо возникнут.

std::floor(2000.0 /*Exactly Representable in 32-bit or 64-bit Floating Point Numbers*/ / 1000.0 /*Also exactly representable*/) --> std::floor(2.0 /*Exactly Representable*/) --> 2
std::ceil(18 / 3 /*both treated as ints, might not even compile if ceil isn't properly overloaded....?*/) --> 6
std::ceil(18.0 /*Exactly Representable*/ / 3.0 /*Exactly Representable*/) --> 6

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

person Xirema    schedule 25.01.2016

Пока значения с плавающей запятой x и y точно представляют целые числа в пределах используемого вами типа, нет проблем --_ 1_ всегда будет давать значение с плавающей запятой, которое точно представляет целочисленный результат. < / strong> Преобразование в int, как вы это делаете, всегда будет работать.

Однако, как только значения с плавающей запятой выходят за пределы диапазона, представляемого целыми числами для типа (Представление целых чисел двойным числом), эпсилоны не помогают.

Рассмотрим этот пример. 16777217 - наименьшее целое число, которое нельзя точно представить как 32-битное float:

int ix=16777217, iy=97;
printf("%d / %d = %d", ix, iy, ix/iy);
// yields "16777217 / 97 = 172961" which is accurate

float x=ix, y=iy;
printf("%f / %f = %f", x, y, x/y);
// yields "16777216.000000 / 97.000000 = 172960.989691"

В этом случае ошибка отрицательная; в других случаях (попробуйте 16777219/1549) ошибка положительная.

Хотя заманчиво добавить эпсилон, чтобы заставить floor работать, это не сильно повысит точность. Если значения различаются на несколько порядков, ошибка становится больше 1, и нельзя гарантировать целочисленную точность. В частности, когда x/y превышает макс. представимо, ошибка может превышать 1.0, поэтому эпсилон не поможет.

Если это произойдет, вам придется подумать об изменении вашего математического подхода - порядка операций, работы с логарифмами и т. Д.

person Jonathan Lidbeck    schedule 20.07.2018

Такие результаты могут появиться при работе с дублями. Вы можете использовать round или вы можете вычесть 0,5, а затем использовать функцию std :: ceil.

person Hakan Kapson    schedule 25.01.2016
comment
Речь идет о вычислениях с операндами и результатами, которые могут быть представлены как целые числа. - person DevSolar; 25.01.2016