Вбудована версія функції повертає інше значення, ніж невбудована версія


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 ++ -швидше це:

1
1
1

3
Чи можете ви надати, який компілятор, параметри компілятора ви використовуєте та яку машину? Для мене добре працює на GCC 7.1 у Windows.
Діодак

31
Чи не ==завжди це трохи непередбачувано зі значеннями з плаваючою комою?
500 - Внутрішня помилка сервера,


2
Ви встановили -Ofastопцію, яка дозволяє проводити такі оптимізації?
cmdLP

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

Відповіді:


73

Пояснення

Деякі компілятори (зокрема 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
}

На відміну від цього прикладу , де ми використовуємо ті самі налаштування компілятора, але надаємо значення під час компіляції, що призводить до більш точної оцінки часу компіляції.


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]) з різними компіляторами та на різних рівнях оптимізації.

Демо в прямому ефірі


Яка мета max()дзвінка? За визначенням, floor(x)менше або дорівнює x, тому max(x, floor(x))завжди буде рівним x.
Ken Thomases

@KenThomases: У цьому конкретному випадку, коли один аргумент лише maxє аргументом floorіншого, він не потрібен. Але я розглянув загальний випадок, коли аргументами maxможуть бути значення або вирази, які не залежать один від одного.
PW

Не слід operator==(double, double)робити саме цього, перевіряти, чи різниця менша за масштабований епсилон? Тоді б не існувало близько 90% питань, пов'язаних із плаваючою комою, щодо SO.
Пітер -

Я думаю, що краще, якщо користувач зможе вказати Epsilonзначення залежно від його конкретних вимог.
PW
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.