tags:
- Cpp
Constexpr in C++ (ENG)
We always want our programs to be faster and faster, and people are constantly searching for algorithms that make our programs run swiftly at runtime. Typically, these algorithms calculate a number or perform other tasks. The lower the time complexity, the better the algorithm.
Since C++11 introduced the constexpr
specifier, we can evaluate the value of entities at compile time, allowing the compiler to determine the results during the compilation process. This can significantly improve runtime performance by shifting some of the work from runtime to compile-time, thus it's called compile-time optimization.
Additionally, with the constexpr
specifier, you can catch errors at compile time, making the code even safer. However, there is a trade-off because performing all calculations at compile-time makes the process compiler-dependent. The result may vary with different compilers.
Let's first go dive in a real example to see how the compile-time optimization happens. This code example following below will serve as our first example today:
#include <iostream>
int add(int a, int b){ // No constant expression
return a + b;
}
int main() {
int a = 100;
int b = 200;
int c = add(a, b);
return 0;
}
This example demonstrates a simple function add
that adds two integers. In the main
function, we call add
with a
and b
as arguments. Currently, the function does not use constexpr
, so the addition is performed at runtime.
After compilation:
add(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
pop rbp
ret
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 100
mov DWORD PTR [rbp-8], 200
mov edx, DWORD PTR [rbp-8]
mov eax, DWORD PTR [rbp-4]
mov esi, edx
mov edi, eax
call add(int, int)
mov DWORD PTR [rbp-12], eax
mov eax, 0
leave
ret
So you can see how many assembly instructions there are needed to be execute in the run-time.
With the using of constexpr
, our C++ code turns into:
#include <iostream>
constexpr int add(int a, int b){
return a + b;
}
int main() {
constexpr int a = 100;
constexpr int b = 200;
constexpr int c = add(a, b);
return 0;
}
After compilation without any compiler optimization:
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], 100
mov DWORD PTR [rbp-8], 200
mov DWORD PTR [rbp-12], 300
mov eax, 0
pop rbp
ret
You see, we don't have the add()
function symbol here. Why? You may have guessed it, the constexpr
specifier implies inline
semantics.
constexpr
The previous example might not be very intuitive, now let's do another experiment to see how awesome constexpr
can be.
#include <iostream>
int fibonacci(int n) {
return (n <= 1) ? n : (fibonacci(n - 1) + fibonacci(n - 2));
}
int main() {
int fib10 = fibonacci(10);
int fib20 = fibonacci(20);
int fib30 = fibonacci(30);
int fib40 = fibonacci(40);
std::cout << "Fibonacci(10): " << fib10 << std::endl;
std::cout << "Fibonacci(20): " << fib20 << std::endl;
std::cout << "Fibonacci(30): " << fib30 << std::endl;
std::cout << "Fibonacci(40): " << fib40 << std::endl;
return 0;
}
This code calculates the Fibonacci sequence at runtime. The output might look like this:
du@DVM:~/cpp$ time ./proc
Fibonacci(10): 55
Fibonacci(20): 6765
Fibonacci(30): 832040
Fibonacci(40): 102334155
real 0m0.604s
user 0m0.600s
sys 0m0.003s
And with constexpr
, our source code look like this:
#include <iostream>
// constexpr function to calculate Fibonacci numbers at compile-time
constexpr int fibonacci(int n) {
return (n <= 1) ? n : (fibonacci(n - 1) + fibonacci(n - 2));
}
int main() {
constexpr int fib10 = fibonacci(10);
constexpr int fib20 = fibonacci(20);
constexpr int fib30 = fibonacci(30);
std::cout << "Fibonacci(10): " << fib10 << std::endl;
std::cout << "Fibonacci(20): " << fib20 << std::endl;
std::cout << "Fibonacci(30): " << fib20 << std::endl;
return 0;
}
With the use of constexpr
, the output might look like this:
du@DVM:~/cpp$ time ./proc
Fibonacci(10): 55
Fibonacci(20): 6765
Fibonacci(30): 832040
Fibonacci(40): 102334155
real 0m0.005s
user 0m0.003s
sys 0m0.002s
You can see how crazy the difference is—in user time, the optimized version is 200 times faster! This demonstrates the significant impact of compile-time optimization using constexpr
.
constexpr
ExplainedWe have demonstrated the main idea about the constexpr
specifier, which is to evaluate the value of the expressions at compile time. This means the compiler will provide a constant value at the compile-time (or literals, if you will). For this reason, the object declared by constexpr
would imply a const
semantics.
For example:
constexpr int i = 100;
i = 200; // error
int j = 100; // j is a regular value, which can be modified at runtime. A constant expression must not depend on runtime values.
/*
there might be some j value modification operations...
so you cannot say the j value down below in the compile-time
*/
constexpr int k = j; // error
Here, we have some points to keep in mind. You must noted the right-hand side of constexpr
variable declaration must be a constant expression. Because everything should be figured out at compile-time, thus everything should be known at compile-time. Otherwise, an error will occur.
Also, we saw the function declared by constexpr
implies an inline
semantics, and since C++17, the static data member declared by constexpr
would imply inline
semantics as well.
#include <iostream>
class myClass {
public:
constexpr static int a = 50;
// Equivalent to inline const static int a = 50;
constexpr int a_square();
};
constexpr int myClass::a_square(){ // myClass::a_square would be inlined
return myClass::a * myClass::a;
}
int main() {
myClass obj;
constexpr int i = obj.a_square();
return 0;
}
And variable/function template can be declared constexpr
too.