Встроенная версия функции возвращает другое значение, чем не встроенная версия

85

Как могут две версии одной и той же функции, различающиеся только тем, что одна является встроенной, а другая - нет, возвращать разные значения? Вот код, который я написал сегодня, и я не уверен, как он работает.

#include <cmath>
#include <iostream>

bool is_cube(double r)
{
    return floor(cbrt(r)) == cbrt(r);
}

bool inline is_cube_inline(double r)
{
    return floor(cbrt(r)) == cbrt(r);
}

int main()
{
    std::cout << (floor(cbrt(27.0)) == cbrt(27.0)) << std::endl;
    std::cout << (is_cube(27.0)) << std::endl;
    std::cout << (is_cube_inline(27.0)) << std::endl;
}

Я бы ожидал, что все выходные данные будут равны 1, но на самом деле он выводит это (g ++ 8.3.1, без флагов):

1
0
1

вместо того

1
1
1

Изменить: clang ++ 7.0.0 выводит это:

0
0
0

и g ++ -Ofast this:

1
1
1
zbrojny120
источник
3
Не могли бы вы указать, какой компилятор, параметры компилятора вы используете и на какой машине? У меня работает нормально на GCC 7.1 в Windows.
Диодак
31
Не ==всегда ли непредсказуемы значения с плавающей запятой?
500 - Внутренняя ошибка сервера
3
связанный stackoverflow.com/questions/588004/…
idclev 463035818 09
2
Вы установили -Ofastпараметр, разрешающий такую ​​оптимизацию?
cmdLP
4
Компилятор возвращает cbrt(27.0)значение, в 0x0000000000000840то время как стандартная библиотека возвращает 0x0100000000000840. Двойники различаются 16-м номером после запятой. Моя система: archlinux4.20 x64 gcc8.2.1 glibc2.28 Проверено этим . Интересно, правы ли gcc или glibc.
KamilCuk

Ответы:

73

Объяснение

Некоторые компиляторы (особенно GCC) используют более высокую точность при оценке выражений во время компиляции. Если выражение зависит только от константных входов и литералов, оно может быть вычислено во время компиляции, даже если выражение не присвоено переменной constexpr. Произойдет это или нет, зависит от:

  • Сложность выражения
  • Порог, который компилятор использует в качестве отсечки при попытке выполнить оценку времени компиляции.
  • Другие эвристики, используемые в особых случаях (например, когда clang удаляет петли)

Если выражение указано явно, как в первом случае, оно имеет меньшую сложность, и компилятор, скорее всего, оценит его во время компиляции.

Точно так же, если функция помечена как встроенная, компилятор с большей вероятностью оценит ее во время компиляции, поскольку встроенные функции повышают порог, при котором может произойти оценка.

Более высокие уровни оптимизации также увеличивают этот порог, как в примере -Ofast, где все выражения оцениваются как истинные в gcc из-за более высокой точности оценки времени компиляции.

Мы можем наблюдать это поведение здесь, в проводнике компилятора. При компиляции с -O1 только функция, помеченная как встроенная, оценивается во время компиляции, но при -O3 обе функции оцениваются во время компиляции.

NB: в примерах компилятора-проводника я использую printfвместо этого iostream, потому что он снижает сложность основной функции, делая эффект более заметным.

Демонстрация, inlineне влияющая на оценку времени выполнения

Мы можем гарантировать, что ни одно из выражений не вычисляется во время компиляции, получая значение из стандартного ввода, и когда мы это делаем, все 3 выражения возвращают false, как показано здесь: https://ideone.com/QZbv6X

#include <cmath>
#include <iostream>

bool is_cube(double r)
{
    return floor(cbrt(r)) == cbrt(r);
}
 
bool inline is_cube_inline(double r)
{
    return floor(cbrt(r)) == cbrt(r);
}

int main()
{
    double value;
    std::cin >> value;
    std::cout << (floor(cbrt(value)) == cbrt(value)) << std::endl; // false
    std::cout << (is_cube(value)) << std::endl; // false
    std::cout << (is_cube_inline(value)) << std::endl; // false
}

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

Х. Антонио Перес
источник
22

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

Один из хороших способов сравнить значения с плавающей запятой - это тест на относительный допуск, описанный в статье: « Пересмотр допусков с плавающей запятой» .

Сначала мы вычисляем значение Epsilon( относительного допуска ), которое в этом случае будет:

double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();

А затем используйте его как во встроенных, так и во встроенных функциях следующим образом:

return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon);

Теперь функции:

bool is_cube(double r)
{
    double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();    
    return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon);
}

bool inline is_cube_inline(double r)
{
    double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();
    return (std::fabs(std::round(std::cbrt(r)) - std::cbrt(r)) < Epsilon);
}

Теперь результат будет таким, как ожидалось ( [1 1 1]) с разными компиляторами и на разных уровнях оптимизации.

Живая демонстрация

PW
источник
Какова цель max()звонка? По определению floor(x)меньше или равно x, поэтому max(x, floor(x))всегда будет равно x.
Ken Thomases
@KenThomases: В этом конкретном случае, когда один аргумент maxявляется аргументом floorдругого, это не требуется. Но я рассматривал общий случай, когда аргументы maxмогут быть значениями или выражениями, независимыми друг от друга.
PW
Не следует ли operator==(double, double)делать именно это, проверить, не меньше ли разница, чем масштабированный эпсилон? Около 90% вопросов, связанных с плавающей запятой, в SO тогда не существовало бы.
Питер - Восстановить Монику
Я думаю, что будет лучше, если пользователь сможет указать Epsilonзначение в зависимости от его конкретных требований.
PW