Как две версии одной и той же функции, отличающиеся только тем, что одна из них встроенная, а другая нет, могут возвращать разные значения? Вот некоторый код, который я написал сегодня, и я не уверен, как он работает.
#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 это:
1
1
1
Я получаю 1 0 0
с помощью GCC 8.2.1 на Arch Linux x86_64 и 0 0 0
с помощью clang 7.0.1.
Разве ==
не всегда немного непредсказуем со значениями с плавающей запятой?
связанные stackoverflow.com/questions/588004/…
@user463035818 user463035818 Этот намек, но я бы ожидал либо 1 1 1
, либо 0 0 0
, но ничего другого (в моей наивной вере в детерминизм). Хотя, что касается математики с плавающей запятой и детерминизма, я недавно прочитал статью... ;-) (Детерминизм с плавающей запятой)
Вы установили опцию -Ofast
, которая разрешает такие оптимизации?
@ user463035818 Я думал об этом, но не вижу никакой связи.
@Diodacus Я использую G++ 8.3.1
не видите никакой связи? Тогда, возможно, перечитайте это снова;). Рассмотрим пример в вопросе. Вы можете получить true
за что-то вроде 0.1 + 0.2 == 0.3
, просто в большинстве случаев вы этого не делаете.
@cmdLP Нет, я не устанавливал никаких флагов. А вот с -Ofast результат 1 1 1
.
@Scheff Некоторое время назад я перестал ожидать чего-либо от арифметики с плавающей запятой;). К сожалению, это пробел, который я должен заполнить в какой-то момент
@ user463035818 Я знаю, я знаю, что числа с плавающей запятой не на 100% точны, но я не понимаю, как inline
связано с этим.
см. другие выходные данные в комментарии, вы также можете получить тот же результат для вызова встроенной функции и вызова невстроенной функции, но другой результат для записи встроенного выражения, поэтому основной эффект, похоже, не вызван inline
@ zbrojny120 zbrojny120 Это может быть даже ошибка компилятора, поскольку я знаю, что операции с плавающей запятой должны вести себя точно так, как определено в IEEE 754.
Я попробовал колиру с g++ 8.2.0
и получил 1 1 1
. Потом я скопировал код и аргументы компилятора в божественная стрела и нашел все эти mov esi, 1
но ни одного обращения к чему-либо другому (кроме операторов потокового вывода). ;-)
Похоже, оптимизированный компилятором ответ (т.е. 1
в случае gcc) отличается от того, что происходит при выполнении кода с использованием библиотечных функций. Это похоже на ошибку компилятора. Выводит ли clang с оптимизацией 1
для встроенной версии?
Компилятор возвращает для cbrt(27.0)
значение 0x0000000000000840
, в то время как стандартная библиотека возвращает 0x0100000000000840
. Двойники отличаются 16-м числом после запятой. Моя система: archlinux4.20 x64 gcc8.2.1 glibc2.28 Проверено с помощью это. Интересно, правильно ли это gcc или glibc.
@ 500-InternalServerError «Непредсказуемый» — это мягко сказано, скорее «не будет работать большую часть времени». Проверка равенства с плавающей запятой для чего-либо, кроме определенных комбинаций битов (ноль, +/-Inf и NaN), — это не просто мгновенный сбой в любом коде, с которым я сталкиваюсь, это также признак того, что я не могу доверять кодировщику. все остальное сделал правильно.
Нет никакой причины (кроме оптимизации производительности за счет точности), по которой cbrt(27.0)
должна возвращать что-либо, кроме 3.0
, поскольку и 3, и 27 точно представляются в виде чисел с плавающей запятой. Однако, хотя IEEE-754 требует, чтобы sqrt()
был правильно округлен для всех входных данных, по-видимому, не гарантирует этого для cbrt()
, поэтому реализациям разрешено давать результаты, которые немного отличаются.
Некоторые компиляторы (особенно GCC) используют более высокую точность при оценке выражений во время компиляции. Если выражение зависит только от постоянных входных данных и литералов, оно может оцениваться во время компиляции, даже если выражение не присвоено переменной constexpr. Произойдет это или нет, зависит от:
Если выражение указано явно, как в первом случае, оно имеет меньшую сложность, и компилятор, скорее всего, вычислит его во время компиляции.
Точно так же, если функция помечена как встроенная, компилятор, скорее всего, оценит ее во время компиляции, потому что встроенные функции повышают порог, при котором может произойти оценка.
Более высокие уровни оптимизации также увеличивают этот порог, как в примере с -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
}
В отличие от этот пример, где мы используем те же настройки компилятора, но предоставляем значение во время компиляции, что приводит к более точной оценке времени компиляции.
Как было замечено, использование оператора ==
для сравнения значений с плавающей запятой привело к разным результатам с разными компиляторами и на разных уровнях оптимизации.
Одним из хороших способов сравнения значений с плавающей запятой является тест относительная устойчивость, описанный в статье: Пересмотр допусков с плавающей запятой.
Сначала мы вычисляем значение 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
.
@KenThomases: В этом конкретном случае, когда один аргумент max
является просто floor
другого, это не требуется. Но я рассмотрел общий случай, когда аргументами max
могут быть значения или выражения, которые не зависят друг от друга.
Разве operator==(double, double)
не следует делать именно это, проверять, чтобы разница была меньше, чем масштабированный эпсилон? Около 90% вопросов, связанных с плавающей запятой на SO, тогда бы не существовало.
Я думаю, что лучше, если пользователь сможет указать значение Epsilon
в зависимости от своих конкретных требований.
Не могли бы вы указать, какой компилятор, параметры компилятора вы используете и на какой машине? У меня работает нормально на GCC 7.1 в Windows.