Lei Mao bio photo

Lei Mao

Machine Learning, Artificial Intelligence, Computer Science.

Twitter Facebook LinkedIn GitHub   G. Scholar E-Mail RSS

Introduction

In some scenarios, our program would call some functions func with the exact same argument N several times. Computing the return value of such functions during runtime might take a while if the cost of computing func(N) is high. However, with the constexpr specifier, we could compute the value of func(N) during compile-time and save it to the program. During runtime, whenever the program sees func(N), it would treat it as a constant and get the value directly from the program without conducting the computation.


In this blog post, I am going to demonstrate the constexpr specifier is capable of accelerate program by computing the value of functions during runtime.

Example

We prepared two sets of functions, fib1 and fib2 are used to compute Fibonacci number, getSize1 and getSize2 are just identity functions. The difference between fib1 and fib2, getSize1 and getSize2, is the constexpr specifier.


The constexpr specifier would try to compute the value of expressions to compile-time constants if it is possible. In this way, the program runs much faster.

// A C++ program to demonstrate use of constexpr 
// To compile the program:
// g++ constexpr.cpp -o constexpr -Werror=vla

#include <iostream> 
#include <chrono>

constexpr long long int fib1(int n) 
{ 
    return (n <= 1)? n : fib1(n-1) + fib1(n-2); 
} 

long long int fib2(int n) 
{ 
    return (n <= 1)? n : fib2(n-1) + fib2(n-2); 
} 

constexpr size_t getSize1(size_t n)
{
    return n;
}

size_t getSize2(size_t n)
{
    return n;
}

int main () 
{
    // This is allowed because getSize1(10) is a constant during compile-time.
    int array1[getSize1(10)];
    // This is not allowed because getSize2(10) is a variable during compile-time.
    // g++ would actually allow this "erroneous" grammar due to VLA.
    // To disable VLA, add `-Werror=vla` to the command for compile.
    // int array2[getSize2(10)];

    std::chrono::steady_clock::time_point time_start;
    std::chrono::steady_clock::time_point time_end;

    // Value of res1 is computed at compile time.  
    time_start = std::chrono::steady_clock::now();
    long long int res1 = fib1(30); 
    time_end = std::chrono::steady_clock::now();
    std::cout << "Time Elapsed: " << std::chrono::duration_cast<std::chrono::nanoseconds>(time_end - time_start).count() << "[ns]" << std::endl;
    std::cout << res1 << std::endl; 

    // Value of res2 needs to be computed during runtime. 
    time_start = std::chrono::steady_clock::now();
    long long int res2 = fib2(30); 
    time_end = std::chrono::steady_clock::now();
    std::cout << "Time Elapsed: " << std::chrono::duration_cast<std::chrono::nanoseconds>(time_end - time_start).count() << "[ns]" << std::endl;
    std::cout << res2 << std::endl; 

    int n;
    std::cout << "Please input a value for computing Fibonacci number: " << std::endl;
    std::cin >> n;
    time_start = std::chrono::steady_clock::now();
    long long int res3 = fib1(n); 
    time_end = std::chrono::steady_clock::now();
    std::cout << "Time Elapsed: " << std::chrono::duration_cast<std::chrono::nanoseconds>(time_end - time_start).count() << "[ns]" << std::endl;
    std::cout << res3 << std::endl; 

    return 0; 
} 

To compile the program, run the following command in the terminal.

$ g++ constexpr.cpp -o constexpr -Werror=vla

Note that -Werror=vla is to disable VLA for g++ for non-standard dynamic array creation.

$ ./constexpr 
Time Elapsed: 360[ns]
832040
Time Elapsed: 5603501[ns]
832040
Please input a value for computing Fibonacci number: 
30
Time Elapsed: 4179985[ns]
832040

We found that the fib1(30) has been treated as a compile-time constant. Therefore, the “computation” of the first Fibonacci number took 360 ns. Because fib2 does not have the constexpr specifier, the fib2(30) needed to be computed during runtime. Therefore, it took 5603501 ns, much longer, to compute the exact same Fibonacci number. Although fib1 does have the constexpr specifier, fib1(n), however, has to be computed during runtime, because n is a runtime variable. Therefore, even though we asked the computer to compute the exact same Fibonacci number, it still took 4179985 ns to run.

Difference to the inline Specifier

The inline specifier could significantly reduce the time of function call overhead. Therefore, if the function is frequently called and fast to compute, we should always consider using the inline specifier. However, the functions are always executed during runtime. constexpr might be better used for functions that takes long to compute because the values might be already computed during compile time.

Conclusions

If you find your program is slow because of repeatedly computing the same constants during runtime. Try to add constexpr to some key functions for those computations.