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

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

#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

Не могли бы вы указать, какой компилятор, параметры компилятора вы используете и на какой машине? У меня работает нормально на GCC 7.1 в Windows.

Diodacus 09.04.2019 12:13

Я получаю 1 0 0 с помощью GCC 8.2.1 на Arch Linux x86_64 и 0 0 0 с помощью clang 7.0.1.

Thomas 09.04.2019 12:15

Разве == не всегда немного непредсказуем со значениями с плавающей запятой?

500 - Internal Server Error 09.04.2019 12:15

связанные stackoverflow.com/questions/588004/…

463035818_is_not_a_number 09.04.2019 12:16

@user463035818 user463035818 Этот намек, но я бы ожидал либо 1 1 1, либо 0 0 0, но ничего другого (в моей наивной вере в детерминизм). Хотя, что касается математики с плавающей запятой и детерминизма, я недавно прочитал статью... ;-) (Детерминизм с плавающей запятой)

Scheff's Cat 09.04.2019 12:18

Вы установили опцию -Ofast, которая разрешает такие оптимизации?

cmdLP 09.04.2019 12:18

@ user463035818 Я думал об этом, но не вижу никакой связи.

zbrojny120 09.04.2019 12:25

@Diodacus Я использую G++ 8.3.1

zbrojny120 09.04.2019 12:26

не видите никакой связи? Тогда, возможно, перечитайте это снова;). Рассмотрим пример в вопросе. Вы можете получить true за что-то вроде 0.1 + 0.2 == 0.3, просто в большинстве случаев вы этого не делаете.

463035818_is_not_a_number 09.04.2019 12:27

@cmdLP Нет, я не устанавливал никаких флагов. А вот с -Ofast результат 1 1 1.

zbrojny120 09.04.2019 12:27

@Scheff Некоторое время назад я перестал ожидать чего-либо от арифметики с плавающей запятой;). К сожалению, это пробел, который я должен заполнить в какой-то момент

463035818_is_not_a_number 09.04.2019 12:29

@ user463035818 Я знаю, я знаю, что числа с плавающей запятой не на 100% точны, но я не понимаю, как inline связано с этим.

zbrojny120 09.04.2019 12:29

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

463035818_is_not_a_number 09.04.2019 12:30

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

cmdLP 09.04.2019 12:31

Я попробовал колиру с g++ 8.2.0 и получил 1 1 1. Потом я скопировал код и аргументы компилятора в божественная стрела и нашел все эти mov esi, 1 но ни одного обращения к чему-либо другому (кроме операторов потокового вывода). ;-)

Scheff's Cat 09.04.2019 12:46

Похоже, оптимизированный компилятором ответ (т.е. 1 в случае gcc) отличается от того, что происходит при выполнении кода с использованием библиотечных функций. Это похоже на ошибку компилятора. Выводит ли clang с оптимизацией 1 для встроенной версии?

KamilCuk 09.04.2019 12:47

Компилятор возвращает для cbrt(27.0) значение 0x0000000000000840, в то время как стандартная библиотека возвращает 0x0100000000000840. Двойники отличаются 16-м числом после запятой. Моя система: archlinux4.20 x64 gcc8.2.1 glibc2.28 Проверено с помощью это. Интересно, правильно ли это gcc или glibc.

KamilCuk 09.04.2019 12:59

@ 500-InternalServerError «Непредсказуемый» — это мягко сказано, скорее «не будет работать большую часть времени». Проверка равенства с плавающей запятой для чего-либо, кроме определенных комбинаций битов (ноль, +/-Inf и NaN), — это не просто мгновенный сбой в любом коде, с которым я сталкиваюсь, это также признак того, что я не могу доверять кодировщику. все остальное сделал правильно.

Graham 09.04.2019 15:11

Нет никакой причины (кроме оптимизации производительности за счет точности), по которой cbrt(27.0) должна возвращать что-либо, кроме 3.0, поскольку и 3, и 27 точно представляются в виде чисел с плавающей запятой. Однако, хотя IEEE-754 требует, чтобы sqrt() был правильно округлен для всех входных данных, по-видимому, не гарантирует этого для cbrt(), поэтому реализациям разрешено давать результаты, которые немного отличаются.

Ilmari Karonen 09.04.2019 16:44
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
85
19
3 799
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Объяснение

Некоторые компиляторы (особенно 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
}

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

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

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

Сначала мы вычисляем значение 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.

Ken Thomases 10.04.2019 04:55

@KenThomases: В этом конкретном случае, когда один аргумент max является просто floor другого, это не требуется. Но я рассмотрел общий случай, когда аргументами max могут быть значения или выражения, которые не зависят друг от друга.

P.W 10.04.2019 06:10

Разве operator==(double, double) не следует делать именно это, проверять, чтобы разница была меньше, чем масштабированный эпсилон? Около 90% вопросов, связанных с плавающей запятой на SO, тогда бы не существовало.

Peter - Reinstate Monica 10.04.2019 12:13

Я думаю, что лучше, если пользователь сможет указать значение Epsilon в зависимости от своих конкретных требований.

P.W 10.04.2019 12:20

Другие вопросы по теме