Как могут две версии одной и той же функции, различающиеся только тем, что одна является встроенной, а другая - нет, возвращать разные значения? Вот код, который я написал сегодня, и я не уверен, как он работает.
#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
==
всегда ли непредсказуемы значения с плавающей запятой?-Ofast
параметр, разрешающий такую оптимизацию?cbrt(27.0)
значение, в0x0000000000000840
то время как стандартная библиотека возвращает0x0100000000000840
. Двойники различаются 16-м номером после запятой. Моя система: archlinux4.20 x64 gcc8.2.1 glibc2.28 Проверено этим . Интересно, правы ли gcc или glibc.Ответы:
Объяснение
Некоторые компиляторы (особенно GCC) используют более высокую точность при оценке выражений во время компиляции. Если выражение зависит только от константных входов и литералов, оно может быть вычислено во время компиляции, даже если выражение не присвоено переменной constexpr. Произойдет это или нет, зависит от:
Если выражение указано явно, как в первом случае, оно имеет меньшую сложность, и компилятор, скорее всего, оценит его во время компиляции.
Точно так же, если функция помечена как встроенная, компилятор с большей вероятностью оценит ее во время компиляции, поскольку встроенные функции повышают порог, при котором может произойти оценка.
Более высокие уровни оптимизации также увеличивают этот порог, как в примере -Ofast, где все выражения оцениваются как истинные в gcc из-за более высокой точности оценки времени компиляции.
Мы можем наблюдать это поведение здесь, в проводнике компилятора. При компиляции с -O1 только функция, помеченная как встроенная, оценивается во время компиляции, но при -O3 обе функции оцениваются во время компиляции.
-O1
: https://godbolt.org/z/u4gh0g-O3
: https://godbolt.org/z/nVK4SoNB: в примерах компилятора-проводника я использую
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 }
В отличие от этого примера , где мы используем те же настройки компилятора, но предоставляем значение во время компиляции, что приводит к более точной оценке времени компиляции.
источник
Как было замечено, использование
==
оператора для сравнения значений с плавающей запятой привело к получению разных результатов с разными компиляторами и на разных уровнях оптимизации.Один из хороших способов сравнить значения с плавающей запятой - это тест на относительный допуск, описанный в статье: « Пересмотр допусков с плавающей запятой» .
Сначала мы вычисляем значение
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]
) с разными компиляторами и на разных уровнях оптимизации.Живая демонстрация
источник
max()
звонка? По определениюfloor(x)
меньше или равноx
, поэтомуmax(x, floor(x))
всегда будет равноx
.max
является аргументомfloor
другого, это не требуется. Но я рассматривал общий случай, когда аргументыmax
могут быть значениями или выражениями, независимыми друг от друга.operator==(double, double)
делать именно это, проверить, не меньше ли разница, чем масштабированный эпсилон? Около 90% вопросов, связанных с плавающей запятой, в SO тогда не существовало бы.Epsilon
значение в зависимости от его конкретных требований.