Search⌘ K
AI Features

Puzzle 23: Explanation

Explore how Rust's const functions enable calculations at compile time to enhance runtime performance. Understand the restrictions on const functions, how they differ from C++ constexpr, and scenarios where using const functions benefits your Rust programs. This lesson helps you grasp the trade-offs and evolving capabilities of compile-time function execution in Rust.

Test it out

Press “Run” to see the output of the code.

C++
const fn fib(n: u128) -> u128 {
let mut a = 1;
let mut b = 1;
for _ in 2..n {
let tmp = a + b;
a = b;
b = tmp;
}
b
}
fn main() {
for i in 0..5 {
println!("Fib {} = {}", i, fib(i));
}
}

Explanation

Marking a function as const causes the function to run at compile time rather than at runtime. When a function runs at compile time, the compiler calculates the results beforehand from constant inputs, which can help speed up complex calculations that we might need later.

Suppose our program requires a lot of Fibonacci numbers. Without a const function, our program would need to recalculate the numbers as needed, possibly more than once. However, by using a const function, we can store these numbers as constant values in our program, dramatically improving its performance.

The const functions are gradually becoming more powerful. However, due to these functions being relatively new, we still can’t use the following features inside of a constant function:

  • Floating-point operations—which we can move around, but can’t work with.
  • Dynamic trait types.
  • Generic bounds on parameters other than Sized.
  • Raw pointer operations.
  • Union (enum) field access.
  • Memory operations such as transmute.

As it turns out, for loops fall into the unavailable category because they require a range. They’re prohibited because of the generic bounds restriction. This makes the example code fail to compile.

Other loop types work fine, though. For example, we can rewrite the example using a while loop:

C++
const fn fib(n: u128) -> u128 {
let mut a = 1;
let mut b = 0;
let mut counter = 0;
while counter < n {
let tmp = a + b;
a = b;
b = tmp;
counter += 1;
}
b
}
fn main() {
for i in 0..5 {
println!("Fib {} = {}", i, fib(i));
}
}

Rust is adding more and more const fn support over time as the compile-time environment is being extended to support it.

Constant guarantees

C++ includes a facility to label a function as constexpr. In C++, this doesn’t guarantee that the function runs at compile time, it merely suggests it. Rust is more strict than that. Constant functions must run at compile time and not during regular program execution. This ensures that we know exactly what resources are being utilized by executing the function. No resources are used at runtime, but it comes at the expense of longer compilation times.

C++ constexpr launched with a very restrictive set of supported features and added to the available functionality over time. Rust is following a similar trajectory.

Using constant functions

As we just learned, constant functions can shift some of the calculation burden to compile time, helping to speed up our program’s execution. Here are a few scenarios in which constant functions are useful:

  • Programs often rely on the results of complex calculations with limited sets of input. With const variables and functions, we can build lookup tables of the required results, skipping the need to perform these calculations at runtime.

  • Sometimes our program relies on a predetermined piece of math, yet showing the work can help explain what the program does. Moving that work to compile time removes the runtime performance penalty for performing the calculation. We’re still free to tweak the math in the source code.


Note: Constant functions have some serious limitations on the data types they can use. An alternative is to write a separate program to calculate a lookup table, and then copy and paste the results into a const variable.