1

This code, when compiled with g++ -O3, does not seem to evaluate get_fibonacci(50) at compile time - as it runs for a very long time.

#include <iostream>

constexpr long long get_fibonacci(int num){
    if(num == 1 || num == 2){return 1;}
    return get_fibonacci(num - 1) + get_fibonacci(num - 2);
}

int main()
{
    std::cout << get_fibonacci(50) << std::endl;
}

Replacing the code with

#include <iostream>

constexpr long long get_fibonacci(int num){
    if(num == 1 || num == 2){return 1;}
    return get_fibonacci(num - 1) + get_fibonacci(num - 2);
}

int main()
{
    long long num = get_fibonacci(50);
    std::cout << num << std::endl;
}

worked perfectly fine. I don't know exactly why this is occurring, but my guess is that get_fibonacci(50) is not evaluated at compile-time in the first scenario because items given std::cout are evaluated at runtime. Is my reasoning correct, or is something else happening? Can somebody please point me in the right direction?

max66
  • 65,235
  • 10
  • 71
  • 111
ahskdjfk
  • 919
  • 5
  • 8
  • 3
    How are you determining whether it gets evaluated at compile time or not? What is the difference in observed behavior between the code that works perfectly fine and the code that doesn't? – Nathan Pierson Nov 20 '20 at 23:07
  • 2
    @NathanPierson: If you compile and run the first version, it will take a [looong time](https://godbolt.org/z/Mr4nfr) (GodBolt; you can also see the mutiple calls to the function). That's how... – einpoklum Nov 20 '20 at 23:09
  • 1
    ... however, OP's second version doesn't ["work perfectly fine" either](https://godbolt.org/z/jxWGvc). – einpoklum Nov 20 '20 at 23:14
  • what compiler version are you using, both versions seem to work when I test them – LZR Nov 20 '20 at 23:15
  • 2
    The runtime complexity is definitely because you aren't memoizing anything, meaning there's a truly tremendous number of calls to `get_fibonacci(num)` per value of `num`. MSVC actually refuses to compile the latter version because of "evaluation exceeding step limit of 1048576". – Nathan Pierson Nov 20 '20 at 23:15
  • Not sure where you are getting this from. Both versions take about 17.5 seconds to run on my machine (gcc 10.2.0). At any rate, nobody ever has promised anything about "compile-time". There are no such words in the standard. – n. m. could be an AI Nov 20 '20 at 23:22
  • C++20 will introduce [`constinit`](https://en.cppreference.com/w/cpp/language/constinit) which is basically `constexpr` but necessarily evaluated at compile time. `constexpr` is allowed to be evaluated at runtime if the result isn't used at compile time. – François Andrieux Nov 21 '20 at 00:10
  • actually that fibonacci algorithm is too bad to be calculated efficiently. `get_fibonacci(n)` needs `2*get_fibonacci(n) - 1` function calls and it grows **exponentially** so take a simple example where `get_fibonacci(n)` needs 2^n calls then the difference between 2^50 and 2^30 is 2^20 which means the latter runs 2^20 = 1048576 times faster. The compiler can't wait for a million times more so it must give up – phuclv Nov 21 '20 at 00:14
  • Declaring a function `constexpr` tells the compiler that the result *may* be evaluated at compile time. The `constexpr` qualifier alone does not specifically *require* evaluation at compile time. Compilers apply various criteria at the call point for deciding if such a function is called/evaluated at compile time. The tricks to convince the compiler, at the call point or in the function itself, are potentially different for different compilers, since it is a quality of implementation concern - the "as if" rule is concerned with visible output, not compile-time or runtime performance. – Peter Nov 21 '20 at 00:38

4 Answers4

3

Actually, both versions of your code do not have the Fibonnaci number computed at compile-time, with typical compilers and compilation flags. But, interestingly enough, if you reduce the 50 to be, say, 30, both versions of your program do have the compile-time evaluation.

Proof: GodBolt

At the link, your first program is compiled and run first with 50 as the argument to get_fibbonacci(), then with 30, using GCC 10.2 and clang 11.0.

What you're seeing is the limits of the compiler's willingness to evaluate code at compile-time. Both compilers engage in the recursive evaluation at compile time - until a certain depth, or certain evaluation time cap, has elapsed. They then give up and leave it for run-time evaluation.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • 1
    One can increase the compile-tine evaluation cutoff with a compiler flag. One also can enforce compile-time evaluation by feeding the function result into a context where a constant expression is required. It is **not** recommended to use these methods to test this Fibonacci implementation, unless your machine has at least 64GB of RAM. Ask me how I know. – n. m. could be an AI Nov 21 '20 at 00:00
  • @n.'pronouns'm.: How do you know? :-) – einpoklum Nov 21 '20 at 08:19
2

I don't know exactly why this is occurring, but my guess is that get_fibonacci(50) is not evaluated at compile-time in the first scenario because items given std::cout are evaluated at runtime

Your function can be computed compile-time, because receive a compile-time know value (50), but can also computed run-time, because the returned value is send to standard output so it's used run-time.

It's a gray area where the compiler can choose both solutions.

To impose (ignoring the as-if rule) the compile-time computation, you can place the returned value in a place where the value is required compile-time.

For example, in a template parameter, in your first example

std::cout << std::integral_constant<long long, get_fibonacci(50)>::value
   << std::endl;

or in a constexpr variable, in your second example

constexpr long long num = get_fibonacci(50);

But remember there is the "as-if rule", so the compiler (in this case, also using constexpr or std::integral_constant) can select the run-time solution because this "do not change the observable behavior of the program".

max66
  • 65,235
  • 10
  • 71
  • 111
  • It is the other way, `std::cout << get_fibonacci(50)` is runtime, but, with as-if rule, can be optimized (either if function is constexpr or not). (Indeed (poor/evil) compiler might replace every `42` usage by `(sleep(50), 6*7)`, but...). – Jarod42 Nov 21 '20 at 00:21
  • @Jarod42 - The output of the value obtained from `get_fibonacci(50)` is run-time, but (ignoring the as-if rule) the computation of the value is necessarily run-time or can be compile-time? – max66 Nov 21 '20 at 12:46
  • I think our wording runtime/compile time (with as-if rule) is a little confusing. Is `return 40 + 2` runtime or compile-time? Even `-O0` (which might be seen as option to disable as-if rule) might do this kind of optimization. From language point of view, there are no runtime/compile time but contant expression/context (and other context). Then if we consider constant expression as compile time, `std::cout << get_fibonacci(50)` is only runtime. as-if rule allows the optimization to have directly the value, without actual call. – Jarod42 Nov 21 '20 at 18:35
  • @Jarod42 - suppose we have a program with, in `main()`, only `std::cout << get_fibonacci(7)`. It prints (run-time) the value `13`. Question: the language rules (and ignoring the as-if rule) impose that the value `13` is computed compile-time or run-time? In other words: the language impose that the executable file contains the value `13`, and no call at all to `get_fibonacci(7)`, or impose that the the executable contains a (recursive) call to `get_fibonacci(7)`? Or, maybe, the language let (also without invoking the as-if rule) the compiler free to choose both solutions? – max66 Nov 21 '20 at 20:28
  • I would say second option (recursive call) and with as-if rule any solution which display 13 (as alternative solution the dynamic programming :-) or even `20 - 7`). – Jarod42 Nov 21 '20 at 22:04
  • @Jarod42 - I was convinced that the standard doesn't impose nothing, so the compiler is free to choose both solution. When I have more time, I'll try to look the standard to understand better. – max66 Nov 21 '20 at 23:02
0

Assign to a constexpr to get the compiler to spit out an error message

constexpr auto val = get_fibonacci(50);
Surt
  • 15,501
  • 3
  • 23
  • 39
0

constexpr functions are evaluated at compile time only in constexpr context, which includes assignment to constexpr variables, template parameter, array size...

Regular function/operator call is not such context.

std::cout << get_fibonacci(50); is done at runtime.

Now, compiler might optimize any (constexpr or not, inline or not) functions with the as-if rule, resulting in a constant, a simpler loop, ...

Jarod42
  • 203,559
  • 14
  • 181
  • 302