Skip to content

← Performance and Data Layout step 20 of 20

Hard End-to-End

const fn and compile-time evaluation

The purest form of “pay at build time, not run time”: code that produces a value during compilation and contributes zero instructions to the running program. And the place where you meet the const-eval engine as what it really is — a language within the language, with its own rules about what may run.

What to write

pub const fn crc32_table() -> [u32; 256]
pub static TABLE: [u32; 256] = crc32_table();
pub fn crc32(data: &[u8]) -> u32

The table is the standard CRC-32 (IEEE 802.3, reflected, polynomial 0xEDB88320). Entry i is computed by starting from i and, eight times:

c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 }

crc32 is the table-driven loop: start at 0xFFFF_FFFF, and for each byte

crc = TABLE[((crc ^ byte) & 0xFF) as usize] ^ (crc >> 8)

then return crc ^ 0xFFFF_FFFF. The classic check value applies: crc32(b"123456789") == 0xCBF4_3926.

The spec also requires a compile-time assertion:

const _: () = assert!(crc32_table()[1] == 0x7707_3096);

The error that stops you first: E0015

You will write the table with a for loop, and it will not compile.

error[E0015]: cannot call non-const method
              `<std::ops::Range<usize> as Iterator>::next` in constant functions

A for loop desugars to IntoIterator::into_iter plus repeated Iterator::nexttrait method calls, and trait dispatch does not run in const context. while does. So does loop. Rewrite the loop with an explicit counter and it works.

That is the shape of the whole const-eval sublanguage: no iterators, no heap, no trait methods (with a growing list of exceptions), no floating-point in some positions, no reading of statics. What it does have is arithmetic, arrays, if, while, match, indexing and mutation of locals — which is enough to build almost any table you want.

Why the assertion calls the function instead of reading TABLE

Because this fails:

const _: () = assert!(TABLE[1] == 0x7707_3096);   // E0013

E0013: constants cannot refer to statics. A const is a value inlined at every use site, while a static is a memory location; letting one read the other would mean the compiler had to reason about a runtime address at compile time. Call crc32_table() again instead — it costs nothing, because it runs at compile time too.

const or static for a table?

static, and it matters.

A const item is not a variable. It is a value inlined at every use site — so const TABLE: [u32; 256] would copy all 1024 bytes into the binary at each mention, and worse, taking &TABLE in two places could give two different addresses. A static has one address and one copy.

This is precisely what clippy’s large_const_arrays lint catches, and it is one of the few clippy lints whose reasoning is about the binary rather than about style.

A pitfall that is genuinely surprising

const fn fib(n: u64) -> u64 {
    if n < 2 { n } else { fib(n - 1) + fib(n - 2) }
}
const F30: u64 = fib(30);

This fails to compile:

error: constant evaluation is taking a long time

The long_running_const_eval lint is deny by default. The const-eval interpreter is a tree-walking interpreter — orders of magnitude slower than the compiled code would be — and it stops you rather than hanging your build. The iterative version computing fib(90) compiles instantly. The lesson is that const-eval is for tables and small computations, not for moving your workload to build time.

Two more modern conveniences

  • const { ... } blocks (stable 1.79) force an expression to be evaluated at compile time, anywhere — including inside a function body. They are the modern way to write a compile-time assertion: const { assert!(N.is_power_of_two()) }.
  • const fn is not the same as “always evaluated at compile time.” A const fn may also be called at runtime, with ordinary arguments, and then it is just a function. const/static initialisers and const { } blocks are what force evaluation.

Errors and lints

E0015 (a non-const call in const context), E0080 (evaluation of a constant value failed — a failing const assert), E0013 (a const referring to a static), E0493 (a destructor cannot run in const context), E0658 (an unstable const feature). Lints: large_const_arrays, missing_const_for_fn (pedantic), missing_const_for_thread_local, and the interior-mutability pair declare_interior_mutable_const and borrow_interior_mutable_const — a const AtomicUsize is a fresh, separate atomic at every use site, which is never what anybody wants.

Remember the grade is compile + tests + clippy -D warnings.