85

How can two versions of the same function, differing only in one being inline and the other one not, return different values? Here is some code I wrote today and I am not sure how it works.

#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;
}

I would expect all outputs to be equal to 1, but it actually outputs this (g++ 8.3.1, no flags):

1
0
1

instead of

1
1
1

Edit: clang++ 7.0.0 outputs this:

0
0
0

and g++ -Ofast this:

1
1
1
chwarr
  • 6,777
  • 1
  • 30
  • 57
zbrojny120
  • 776
  • 6
  • 10
  • 3
    Can you please provide what compiler, compiler options are you using and what machine ? Works ok for me on GCC 7.1 on Windows. – Diodacus Apr 09 '19 at 10:13
  • I get `1 0 0` using GCC 8.2.1 on Arch Linux x86_64 and `0 0 0` using clang 7.0.1. – Thomas Apr 09 '19 at 10:15
  • 31
    Isn't `==` always a bit unpredictable with floating point values? – 500 - Internal Server Error Apr 09 '19 at 10:15
  • 3
    related https://stackoverflow.com/questions/588004/is-floating-point-math-broken – 463035818_is_not_an_ai Apr 09 '19 at 10:16
  • @user463035818 That hint in mind, but I would expect either `1 1 1` or `0 0 0` but nothing else (in my naive belief in deterministics). Though, regarding floating point math and deterministics, I recently read an article... ;-) ([Floating-Point Determinism](https://randomascii.wordpress.com/2013/07/16/floating-point-determinism/)) – Scheff's Cat Apr 09 '19 at 10:18
  • 2
    Did you set the `-Ofast` option, which allows such optimizations? – cmdLP Apr 09 '19 at 10:18
  • @user463035818 I thought about this, but I don't really see any connection. – zbrojny120 Apr 09 '19 at 10:25
  • @Diodacus I am using G++ 8.3.1 – zbrojny120 Apr 09 '19 at 10:26
  • dont see any connection? Then maybe read it again ;). Consider the example in the question. You can get `true` for something like `0.1 + 0.2 == 0.3`, it is just that most of the time you dont – 463035818_is_not_an_ai Apr 09 '19 at 10:27
  • @cmdLP No, I did not set any flags. But with -Ofast the result is `1 1 1`. – zbrojny120 Apr 09 '19 at 10:27
  • @Scheff I stopped expecting anything from floating point arithmetics some time ago ;). Unfortunately it is a gap that I should fill at some point – 463035818_is_not_an_ai Apr 09 '19 at 10:29
  • @user463035818 I know, I am aware that floating point numbers are not 100% precise, but I don't see how `inline` is related to this. – zbrojny120 Apr 09 '19 at 10:29
  • see the other outputs in the comment, you can get also same result for the inline function call and the non-inline function call, but different result for writing the expression inline, so the main effect seems not to be caused by `inline` – 463035818_is_not_an_ai Apr 09 '19 at 10:30
  • 1
    @zbrojny120 That might even be a compiler bug, as I know, floating point operations should exactly behave as defined with IEEE 754. – cmdLP Apr 09 '19 at 10:31
  • I tried on [**coliru**](http://coliru.stacked-crooked.com/a/60be54bb3a52666f) with `g++ 8.2.0` and got `1 1 1`. Then I copied the code and compiler arguments to [**godbolt**](https://gcc.godbolt.org/z/TfRpVj) and found all these `mov esi, 1` but not a single call to anything else (except stream output operators). ;-) – Scheff's Cat Apr 09 '19 at 10:46
  • Looks like optimized by the compiler answer (ie. `1` in case of gcc) differs from what happens when executing the code using library functions. This looks like a compiler bug. Does clang with optimization also outputs `1` for the inlined version? – KamilCuk Apr 09 '19 at 10:47
  • 4
    Compiler returns for `cbrt(27.0)` the value of `0x0000000000000840` while the standard library returns `0x0100000000000840`. The doubles differ in 16th number after comma. My system: archlinux4.20 x64 gcc8.2.1 glibc2.28 Checked with [this](https://godbolt.org/z/zaZEUt). Wonder if gcc or glibc is right. – KamilCuk Apr 09 '19 at 10:59
  • 1
    @500-InternalServerError "Unpredictable" is putting it mildly - "will not work most of the time" is more like it. Checking floating-point equality for anything other than defined bit combinations (zero, +/-Inf and NaN) is not just an instant fail on any code I come across, it's also an indication that I can't trust the the coder to have done anything else right. – Graham Apr 09 '19 at 13:11
  • 2
    There is no reason (other than performance optimizations at the expense of accuracy) why `cbrt(27.0)` should return anything but `3.0`, since both 3 and 27 are exactly representable in floating point. However, while IEEE-754 [requires `sqrt()` to be correctly rounded](https://stackoverflow.com/questions/4317988/ieee-754-floating-point-precision-how-much-error-is-allowed) for all inputs, it apparently doesn't guarantee that for `cbrt()`, so implementations are allowed to produce results that are slightly off. – Ilmari Karonen Apr 09 '19 at 14:44

2 Answers2

73

Explanation

Some compilers (notably GCC) use higher precision when evaluating expressions at compile time. If an expression depends only on constant inputs and literals, it may be evaluated at compile time even if the expression is not assigned to a constexpr variable. Whether or not this occurs depends on:

  • The complexity of the expression
  • The threshold the compiler uses as a cutoff when attempting to perform compile time evaluation
  • Other heuristics used in special cases (such as when clang elides loops)

If an expression is explicitly provided, as in the first case, it has lower complexity and the compiler is likely to evaluate it at compile time.

Similarly, if a function is marked inline, the compiler is more likely to evaluate it at compile time because inline functions raise the threshold at which evaluation can occur.

Higher optimization levels also increase this threshold, as in the -Ofast example, where all expressions evaluate to true on gcc due to higher precision compile-time evaluation.

We can observe this behavior here on compiler explorer. When compiled with -O1, only the function marked inline is evaluated at compile-time, but at -O3 both functions are evaluated at compile-time.

NB: In the compiler-explorer examples, I use printf instead iostream because it reduces the complexity of the main function, making the effect more visible.

Demonstrating that inline doesn’t affect runtime evaluation

We can ensure that none of the expressions are evaluated at compile time by obtaining value from standard input, and when we do this, all 3 expressions return false as demonstrated here: 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
}

Contrast with this example, where we use the same compiler settings but provide the value at compile-time, resulting in the higher-precision compile-time evaluation.

Alecto Irene Perez
  • 10,321
  • 23
  • 46
22

As observed, using the == operator to compare floating point values has resulted in different outputs with different compilers and at different optimization levels.

One good way to compare floating point values is the relative tolerance test outlined in the article: Floating-point tolerances revisited.

We first calculate the Epsilon (the relative tolerance) value which in this case would be:

double Epsilon = std::max(std::cbrt(r), std::floor(std::cbrt(r))) * std::numeric_limits<double>::epsilon();

And then use it in both the inline and non-inline functions in this manner:

return (std::fabs(std::floor(std::cbrt(r)) - std::cbrt(r)) < Epsilon);

The functions now are:

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);
}

Now the output will be as expected ([1 1 1]) with different compilers and at different optimization levels.

Live demo

P.W
  • 26,289
  • 6
  • 39
  • 76
  • What's the purpose of the `max()` call? By definition, `floor(x)` is less than or equal to `x`, so `max(x, floor(x))` will always equal `x`. – Ken Thomases Apr 10 '19 at 02:55
  • @KenThomases: In this particular case, where one argument to `max` is just the `floor` of the other, it is not required. But I considered a general case where arguments to `max` can be values or expressions which are independent of each other. – P.W Apr 10 '19 at 04:10
  • Shouldn't `operator==(double, double)` do exactly that, check for the difference being smaller than a scaled epsilon? About 90% of floating point related questions on SO wouldn't exist then. – Peter - Reinstate Monica Apr 10 '19 at 10:13
  • I think it is better if the user gets to specify the `Epsilon` value depending on their particular requirement. – P.W Apr 10 '19 at 10:20