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

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

#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

Редактировать: клан g++ 7.0.0 выводит это:

0
0
0

и g++ -Ofast это:

1
1
1

Ответы

Ответ 1

объяснение

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

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

Ответ 2

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

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

Сначала мы рассчитываем значение 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]) с разными компиляторами и с разными уровнями оптимизации.

Live демо