Compile-time computation
Const evaluation (CTFE - Compile-Time Function Evaluation) is Rust's ability to execute code during compilation rather than at runtime. Through const fn, const generics, and const blocks, you can perform arbitrarily complex computations at compile time, embedding the results directly into your binary. This eliminates initialization overhead, enables compile-time validation, and moves computation cost from every program execution to a single compilation.
After optimizing performance-critical systems across a millennium—from embedded firmware with nanosecond boot requirements to cryptographic implementations demanding constant-time guarantees—I've learned that const evaluation is the ultimate performance optimization: work that happens at compile time costs zero at runtime. It's why embedded systems can generate lookup tables without ROM initialization code, and why security-critical applications can validate configurations before a single instruction executes.
// Ordinary function - runs at runtime
fn factorial_runtime(n: u32) -> u32 {
if n == 0 { 1 } else { n * factorial_runtime(n - 1) }
}
// Const function - can run at compile time
const fn factorial(n: u32) -> u32 {
if n == 0 { 1 } else { n * factorial(n - 1) }
}
// Computed at compile time - zero runtime cost
const FACT_10: u32 = factorial(10); // Result: 3628800 embedded in binary
// Can also be called at runtime if needed
fn main() {
let runtime_value = factorial(5); // Computed at runtime: 120
// FACT_10 is a constant - no computation happens here
println!("10! = {}", FACT_10); // Just loads constant from binary
}
The CTFE interpreter executes const functions during compilation, evaluating expressions, loops, and function calls to produce compile-time constants. This isn't macro expansion—it's actual code execution in an interpreted environment within the compiler.
const { ... }assert! in const context---
In embedded systems and network protocols, CRC (Cyclic Redundancy Check) computations benefit enormously from lookup tables. Generating these tables at compile time eliminates initialization code and reduces boot time to zero.
The Challenge: CRC32 computation requires 256-entry lookup table. Generating it at runtime wastes flash space on initialization code and adds startup latency. Pre-computing by hand is error-prone. The Solution: Generate the entire CRC table at compile time using const evaluation./// CRC32 polynomial (IEEE 802.3)
const CRC32_POLYNOMIAL: u32 = 0xEDB88320;
/// Generate CRC32 lookup table at compile time
const fn generate_crc_table() -> [u32; 256] {
let mut table = [0u32; 256];
let mut i = 0;
while i < 256 {
let mut crc = i as u32;
let mut j = 0;
while j < 8 {
if crc & 1 == 1 {
crc = (crc >> 1) ^ CRC32_POLYNOMIAL;
} else {
crc >>= 1;
}
j += 1;
}
table[i] = crc;
i += 1;
}
table
}
/// Table embedded directly in binary - zero initialization cost
const CRC_TABLE: [u32; 256] = generate_crc_table();
/// Fast CRC32 computation using compile-time generated table
pub fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xFFFFFFFF;
for &byte in data {
let index = ((crc ^ byte as u32) & 0xFF) as usize;
crc = (crc >> 8) ^ CRC_TABLE[index];
}
!crc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crc32() {
// "123456789" has known CRC32: 0xCBF43926
assert_eq!(crc32(b"123456789"), 0xCBF43926);
}
#[test]
fn verify_table_generation() {
// Verify first few entries are correct
assert_eq!(CRC_TABLE[0], 0x00000000);
assert_eq!(CRC_TABLE[1], 0x77073096);
assert_eq!(CRC_TABLE[255], 0x2D02EF8D);
}
}
Impact:
This pattern is ubiquitous in embedded systems: sine/cosine tables for DSP, gamma correction tables for displays, and hash function constants all benefit from compile-time generation.
Systems programming often involves configuration constants with strict requirements: buffer sizes must be powers of two, array dimensions must align, memory regions must not overlap. Catching violations at compile time prevents runtime panics and security vulnerabilities.
The Challenge: Runtime validation of configuration adds overhead and can fail in production. Manual verification is error-prone and doesn't prevent bugs. The Solution: Use const evaluation to enforce invariants at compile time./// Compile-time validation functions
const fn is_power_of_two(n: usize) -> bool {
n > 0 && (n & (n - 1)) == 0
}
const fn validate_alignment(addr: usize, align: usize) -> usize {
assert!(is_power_of_two(align), "alignment must be power of two");
assert!(addr % align == 0, "address not aligned");
addr
}
/// Ring buffer configuration with compile-time validation
struct RingBuffer<T, const SIZE: usize> {
data: [T; SIZE],
read: usize,
write: usize,
}
impl<T, const SIZE: usize> RingBuffer<T, SIZE> {
/// Validates SIZE at compile time
const VALIDATED_SIZE: usize = {
assert!(SIZE > 0, "ring buffer size must be positive");
assert!(is_power_of_two(SIZE), "ring buffer size must be power of two");
assert!(SIZE <= 65536, "ring buffer size too large");
SIZE
};
pub const fn new() -> Self
where
T: Copy,
{
// Force validation by referencing VALIDATED_SIZE
let _ = Self::VALIDATED_SIZE;
// Safe because SIZE is validated to be power of two
Self {
data: unsafe { std::mem::zeroed() },
read: 0,
write: 0,
}
}
/// Fast modulo using bitwise AND (only works for power of two)
#[inline]
fn mask(&self, value: usize) -> usize {
value & (SIZE - 1)
}
}
/// Memory region with compile-time alignment validation
struct MemoryRegion<const ADDR: usize, const SIZE: usize, const ALIGN: usize> {
_marker: std::marker::PhantomData<[u8; SIZE]>,
}
impl<const ADDR: usize, const SIZE: usize, const ALIGN: usize>
MemoryRegion<ADDR, SIZE, ALIGN>
{
const VALIDATED_REGION: () = {
// Validate at compile time
let _ = validate_alignment(ADDR, ALIGN);
assert!(SIZE > 0, "memory region size must be positive");
assert!(SIZE % ALIGN == 0, "size must be multiple of alignment");
};
pub const fn new() -> Self {
let _ = Self::VALIDATED_REGION;
Self { _marker: std::marker::PhantomData }
}
pub const fn base_addr(&self) -> usize { ADDR }
pub const fn size(&self) -> usize { SIZE }
}
// Example: DMA buffer at specific address with strict alignment
type DmaBuffer = MemoryRegion<0x2000_0000, 4096, 64>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_configurations() {
// These compile successfully
let _rb: RingBuffer<u8, 256> = RingBuffer::new();
let _dma: DmaBuffer = DmaBuffer::new();
}
// These would fail to compile:
// let _rb: RingBuffer<u8, 100> = RingBuffer::new(); // Not power of two
// type BadDma = MemoryRegion<0x2000_0001, 4096, 64>; // Misaligned address
}
Impact:
I've seen this pattern prevent countless production incidents in embedded systems, where invalid configurations could cause hardware damage or security vulnerabilities.
Cryptographic implementations require numerous constants, lookup tables, and transformation matrices. Generating these at compile time ensures correctness, enables verification, and eliminates initialization vectors that could be attack surfaces.
The Challenge: Cryptographic S-boxes and round constants must be exact. Runtime generation adds attack surface and initialization cost. Hardcoding by hand introduces transcription errors. The Solution: Generate all cryptographic constants at compile time with verified algorithms./// AES S-box generation at compile time
const fn aes_sbox() -> [u8; 256] {
let mut sbox = [0u8; 256];
let mut p = 1u8;
let mut q = 1u8;
loop {
// Multiply in GF(2^8)
p = p ^ (p << 1) ^ if p & 0x80 != 0 { 0x1B } else { 0 };
// Divide in GF(2^8)
q ^= q << 1;
q ^= q << 2;
q ^= q << 4;
q ^= if q & 0x80 != 0 { 0x09 } else { 0 };
// Compute affine transformation
let xformed = q ^ (q.rotate_left(1)) ^ (q.rotate_left(2))
^ (q.rotate_left(3)) ^ (q.rotate_left(4)) ^ 0x63;
sbox[p as usize] = xformed;
if p == 1 { break; }
}
sbox[0] = 0x63;
sbox
}
/// AES inverse S-box
const fn aes_inv_sbox() -> [u8; 256] {
let sbox = aes_sbox();
let mut inv_sbox = [0u8; 256];
let mut i = 0;
while i < 256 {
inv_sbox[sbox[i] as usize] = i as u8;
i += 1;
}
inv_sbox
}
/// SHA-256 round constants (first 32 bits of fractional parts of cube roots)
const fn sha256_k() -> [u32; 64] {
// In real implementation, these would be computed from cube roots
// Simplified here for clarity
[
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
// ... 56 more constants ...
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
]
}
/// Tables embedded in binary at compile time
const AES_SBOX: [u8; 256] = aes_sbox();
const AES_INV_SBOX: [u8; 256] = aes_inv_sbox();
const SHA256_K: [u32; 64] = sha256_k();
/// Constant-time AES SubBytes operation
pub fn sub_bytes(state: &mut [u8; 16]) {
for byte in state.iter_mut() {
*byte = AES_SBOX[*byte as usize];
}
}
/// Verify S-box properties at compile time
const _: () = {
let sbox = aes_sbox();
let inv_sbox = aes_inv_sbox();
// Verify S-box is a permutation (all values appear exactly once)
let mut seen = [false; 256];
let mut i = 0;
while i < 256 {
assert!(!seen[sbox[i] as usize], "S-box not a permutation");
seen[sbox[i] as usize] = true;
i += 1;
}
// Verify inverse relationship
let mut j = 0;
while j < 256 {
assert!(inv_sbox[sbox[j] as usize] == j as u8, "S-box inverse incorrect");
j += 1;
}
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_aes_sbox() {
// Known AES S-box values
assert_eq!(AES_SBOX[0x00], 0x63);
assert_eq!(AES_SBOX[0x01], 0x7c);
assert_eq!(AES_SBOX[0x53], 0xed);
}
#[test]
fn verify_sbox_inverse() {
for i in 0..256u8 {
assert_eq!(AES_INV_SBOX[AES_SBOX[i as usize] as usize], i);
}
}
}
Impact:
This approach is critical in security-sensitive code where any deviation from specification could introduce vulnerabilities.
Network protocols involve magic numbers, checksum algorithms, and header validations that benefit from compile-time computation and verification. IP addresses, port numbers, and protocol constants can be validated at compile time.
The Challenge: Network protocols have strict formats. Runtime parsing of constants wastes cycles. Typos in hardcoded values cause subtle bugs. The Solution: Parse and validate network constants at compile time./// Parse IPv4 address at compile time
const fn parse_ipv4(s: &str) -> [u8; 4] {
let bytes = s.as_bytes();
let mut octets = [0u8; 4];
let mut octet_idx = 0;
let mut current = 0u8;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'.' {
assert!(octet_idx < 3, "too many dots in IP address");
octets[octet_idx] = current;
octet_idx += 1;
current = 0;
} else if b >= b'0' && b <= b'9' {
let digit = b - b'0';
let new_value = current * 10 + digit;
assert!(new_value <= 255, "octet value exceeds 255");
current = new_value;
} else {
panic!("invalid character in IP address");
}
i += 1;
}
assert!(octet_idx == 3, "incomplete IP address");
octets[octet_idx] = current;
octets
}
/// Compile-time validated IP addresses
const LOCALHOST: [u8; 4] = parse_ipv4("127.0.0.1");
const BROADCAST: [u8; 4] = parse_ipv4("255.255.255.255");
const PRIVATE_NET: [u8; 4] = parse_ipv4("192.168.1.1");
// Compile error: const INVALID: [u8; 4] = parse_ipv4("256.1.1.1");
/// Network byte order conversion at compile time
const fn htons(port: u16) -> u16 {
port.to_be()
}
/// Well-known ports as compile-time constants
const HTTP_PORT: u16 = htons(80);
const HTTPS_PORT: u16 = htons(443);
const SSH_PORT: u16 = htons(22);
/// Internet checksum generation at compile time
const fn internet_checksum(data: &[u8]) -> u16 {
let mut sum = 0u32;
let mut i = 0;
// Sum 16-bit words
while i + 1 < data.len() {
let word = ((data[i] as u32) << 8) | (data[i + 1] as u32);
sum += word;
i += 2;
}
// Add remaining byte if odd length
if i < data.len() {
sum += (data[i] as u32) << 8;
}
// Fold 32-bit sum to 16 bits
while sum >> 16 != 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!(sum as u16)
}
/// Protocol magic numbers with compile-time verification
struct ProtocolHeader<const MAGIC: u32> {
magic: u32,
length: u16,
checksum: u16,
}
impl<const MAGIC: u32> ProtocolHeader<MAGIC> {
const VALIDATED_MAGIC: () = {
assert!(MAGIC != 0, "magic number cannot be zero");
assert!(MAGIC != 0xFFFFFFFF, "magic number cannot be all ones");
// Ensure magic has good Hamming distance for error detection
let bits = MAGIC.count_ones();
assert!(bits >= 8 && bits <= 24, "magic number should have balanced bit pattern");
};
pub fn new(length: u16) -> Self {
let _ = Self::VALIDATED_MAGIC;
Self {
magic: MAGIC,
length,
checksum: 0,
}
}
pub fn validate(&self) -> bool {
self.magic == MAGIC
}
}
/// Custom protocol with validated magic number
type MyProtocol = ProtocolHeader<0xDEADBEEF>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ipv4_parsing() {
assert_eq!(LOCALHOST, [127, 0, 0, 1]);
assert_eq!(BROADCAST, [255, 255, 255, 255]);
assert_eq!(PRIVATE_NET, [192, 168, 1, 1]);
}
#[test]
fn test_port_conversion() {
assert_eq!(HTTP_PORT.to_be(), 80);
assert_eq!(HTTPS_PORT.to_be(), 443);
}
#[test]
fn test_checksum() {
const DATA: &[u8] = b"test";
const CHECKSUM: u16 = internet_checksum(DATA);
assert_ne!(CHECKSUM, 0); // Valid checksum computed
}
}
Impact:
This pattern shines in embedded networking stacks and high-performance packet processing where every cycle counts.
Const generics enable type-safe, compile-time-sized data structures without heap allocation. Mathematical libraries, computer graphics, and scientific computing benefit from compile-time dimension checking and size optimization.
The Challenge: Fixed-size matrices need compile-time dimensions for performance and type safety. Dynamic sizing adds runtime overhead and prevents dimension mismatch detection. The Solution: Use const generics to create matrices with compile-time dimensions and operations.use std::ops::{Add, Mul};
/// Matrix with compile-time dimensions
#[derive(Debug, Clone, Copy, PartialEq)]
struct Matrix<T, const ROWS: usize, const COLS: usize> {
data: [[T; COLS]; ROWS],
}
impl<T, const ROWS: usize, const COLS: usize> Matrix<T, ROWS, COLS> {
/// Create matrix from 2D array
pub const fn new(data: [[T; COLS]; ROWS]) -> Self {
Self { data }
}
/// Get element at position
pub const fn get(&self, row: usize, col: usize) -> &T {
&self.data[row][col]
}
/// Create zero matrix (requires Copy + Default)
pub const fn zero() -> Self
where
T: Copy + Default,
{
Self {
data: [[T::default(); COLS]; ROWS],
}
}
}
impl<T, const N: usize> Matrix<T, N, N> {
/// Create identity matrix at compile time
pub const fn identity() -> Self
where
T: Copy + Default + ~const From<u8>,
{
let mut data = [[T::default(); N]; N];
let mut i = 0;
while i < N {
data[i][i] = T::from(1);
i += 1;
}
Self { data }
}
}
/// Matrix addition (dimensions must match at compile time)
impl<T, const ROWS: usize, const COLS: usize> Add for Matrix<T, ROWS, COLS>
where
T: Add<Output = T> + Copy,
{
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let mut result = self;
for i in 0..ROWS {
for j in 0..COLS {
result.data[i][j] = self.data[i][j] + rhs.data[i][j];
}
}
result
}
}
/// Matrix multiplication (dimensions must be compatible at compile time)
impl<T, const M: usize, const N: usize, const P: usize>
Mul<Matrix<T, N, P>> for Matrix<T, M, N>
where
T: Mul<Output = T> + Add<Output = T> + Copy + Default,
{
type Output = Matrix<T, M, P>;
fn mul(self, rhs: Matrix<T, N, P>) -> Self::Output {
let mut result = Matrix::zero();
for i in 0..M {
for j in 0..P {
let mut sum = T::default();
for k in 0..N {
sum = sum + self.data[i][k] * rhs.data[k][j];
}
result.data[i][j] = sum;
}
}
result
}
}
/// Transformation matrices at compile time
impl Matrix<f32, 4, 4> {
/// Translation matrix
pub const fn translation(x: f32, y: f32, z: f32) -> Self {
Self::new([
[1.0, 0.0, 0.0, x],
[0.0, 1.0, 0.0, y],
[0.0, 0.0, 1.0, z],
[0.0, 0.0, 0.0, 1.0],
])
}
/// Scaling matrix
pub const fn scaling(x: f32, y: f32, z: f32) -> Self {
Self::new([
[x, 0.0, 0.0, 0.0],
[0.0, y, 0.0, 0.0],
[0.0, 0.0, z, 0.0],
[0.0, 0.0, 0.0, 1.0],
])
}
}
/// Type aliases for common matrix dimensions
type Matrix2x2<T> = Matrix<T, 2, 2>;
type Matrix3x3<T> = Matrix<T, 3, 3>;
type Matrix4x4<T> = Matrix<T, 4, 4>;
type Vector3<T> = Matrix<T, 3, 1>;
/// Compile-time transformation matrices for graphics
const IDENTITY_4X4: Matrix4x4<f32> = Matrix::new([
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
]);
const SCALE_2X: Matrix4x4<f32> = Matrix::scaling(2.0, 2.0, 2.0);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_matrix_dimensions() {
let m1: Matrix2x2<i32> = Matrix::new([[1, 2], [3, 4]]);
let m2: Matrix2x2<i32> = Matrix::new([[5, 6], [7, 8]]);
let result = m1 + m2;
assert_eq!(result.data, [[6, 8], [10, 12]]);
}
#[test]
fn test_matrix_multiplication() {
let m1: Matrix<i32, 2, 3> = Matrix::new([[1, 2, 3], [4, 5, 6]]);
let m2: Matrix<i32, 3, 2> = Matrix::new([[7, 8], [9, 10], [11, 12]]);
let result = m1 * m2; // Result is 2x2
// [[58, 64], [139, 154]]
assert_eq!(*result.get(0, 0), 58);
assert_eq!(*result.get(1, 1), 154);
}
#[test]
fn test_incompatible_dimensions() {
// These would fail to compile:
// let m1: Matrix<i32, 2, 3> = Matrix::new([[1, 2, 3], [4, 5, 6]]);
// let m2: Matrix<i32, 2, 2> = Matrix::new([[1, 2], [3, 4]]);
// let result = m1 * m2; // Compile error: dimension mismatch
}
#[test]
fn test_identity_matrix() {
assert_eq!(IDENTITY_4X4.data[0][0], 1.0);
assert_eq!(IDENTITY_4X4.data[1][1], 1.0);
assert_eq!(IDENTITY_4X4.data[0][1], 0.0);
}
}
Impact:
Graphics engines, physics simulations, and machine learning inference all benefit from compile-time dimension checking and the elimination of runtime bounds checking.
---
Rust's const evaluation happens in a special interpreter called Miri (MIR Interpreter) that executes MIR (Mid-level Intermediate Representation) during compilation. Unlike macro expansion (textual substitution), CTFE actually runs code:
const fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
n => fibonacci(n - 1) + fibonacci(n - 2),
}
}
// The compiler executes fibonacci(10) during compilation
const FIB_10: u32 = fibonacci(10); // Result: 55
// Generated assembly contains just:
// mov eax, 55
The interpreter tracks:
As of Rust 1.82, const fn supports increasingly complex operations:
const fn complex_computation() -> u32 {
// Control flow
if some_condition() { 1 } else { 2 }
// Loops
let mut sum = 0;
let mut i = 0;
while i < 10 {
sum += i;
i += 1;
}
// Pattern matching
match sum {
0..=10 => 1,
11..=20 => 2,
_ => 3,
}
// Function calls (to other const fns)
helper_const_fn(sum)
// Arithmetic and bitwise operations
// References and borrowing
// Mutable variables (within const fn scope)
}
const fn helper_const_fn(x: u32) -> u32 {
x * 2
}
const fn some_condition() -> bool {
true
}
Not allowed in const fn (as of Rust 1.82):
const fn invalid_operations() {
// Heap allocation
// let v = Vec::new(); // Error
// Raw pointer dereferencing (mostly)
// unsafe { *some_ptr } // Error (with exceptions)
// Interior mutability
// let cell = Cell::new(5); // Error
// Function pointers (mostly)
// let f: fn() = some_func; // Error
// Trait object calls
// let obj: &dyn Trait = ...; // Error
// Inline assembly
// asm!("nop"); // Error
// Floating point operations (mostly allowed now!)
// Most float ops are now const as of 1.82
}
The const fn feature set expands with each Rust version, gradually approaching full Rust language support.
Const generics allow using values (not just types) as generic parameters:
// Array wrapper with compile-time size
struct Array<T, const N: usize> {
data: [T; N],
}
impl<T, const N: usize> Array<T, N> {
// Const generic in method
pub fn new(data: [T; N]) -> Self {
Self { data }
}
// Const expressions in bounds
pub fn split_half(&self) -> (&[T], &[T])
where
[T; N / 2]: Sized, // Requires N to be even
{
self.data.split_at(N / 2)
}
}
// Different sizes are different types
let arr1: Array<i32, 10> = Array::new([0; 10]);
let arr2: Array<i32, 20> = Array::new([0; 20]);
// arr1 and arr2 are incompatible types
Const generic expressions (stabilized in recent Rust):
struct Buffer<const N: usize> {
// Can use const expressions
data: [u8; N],
checksum: [u8; N / 8], // Const expression
}
impl<const N: usize> Buffer<N> {
const VALIDATED: () = {
assert!(N > 0, "buffer size must be positive");
assert!(N % 8 == 0, "buffer size must be multiple of 8");
};
}
Const blocks allow inline const evaluation:
fn process_data() {
// Const block: evaluated at compile time
const LOOKUP: [u32; 256] = const {
let mut table = [0u32; 256];
let mut i = 0;
while i < 256 {
table[i] = i as u32 * i as u32;
i += 1;
}
table
};
// LOOKUP is embedded as constant
for i in 0..256 {
println!("{}", LOOKUP[i]);
}
}
Const blocks are useful for:
Use assert! in const context for compile-time validation:
const fn validate_config(size: usize, align: usize) {
assert!(size > 0, "size must be positive");
assert!(align.is_power_of_two(), "alignment must be power of two");
assert!(size % align == 0, "size must be multiple of alignment");
}
struct Config<const SIZE: usize, const ALIGN: usize> {
const VALIDATED: () = validate_config(SIZE, ALIGN);
_marker: std::marker::PhantomData<[u8; SIZE]>,
}
// Valid configuration compiles
type ValidConfig = Config<1024, 64>;
// Invalid configuration causes compile error
// type InvalidConfig = Config<1000, 64>; // Error: size not multiple of alignment
Custom compile-time error messages:
const fn require_power_of_two(n: usize) {
if !n.is_power_of_two() {
panic!("Value must be power of two for optimal performance");
}
}
const BUFFER_SIZE: usize = {
const SIZE: usize = 1000;
require_power_of_two(SIZE); // Compile error with custom message
SIZE
};
Const traits allow trait methods to be const:
#![feature(const_trait_impl)]
#[const_trait]
trait ConstAdd {
fn add(&self, other: &Self) -> Self;
}
impl const ConstAdd for i32 {
fn add(&self, other: &Self) -> Self {
*self + *other
}
}
const fn add_numbers<T: ~const ConstAdd>(a: &T, b: &T) -> T {
a.add(b)
}
const RESULT: i32 = add_numbers(&5, &10); // Evaluated at compile time
This feature is experimental but enables generic const functions over traits.
Rust automatically promotes some expressions to const:
fn example() {
// This string is promoted to static
let s: &'static str = "hello";
// This reference is promoted to static
let r: &'static i32 = &42;
// These work because values are promoted
const STRS: &[&str] = &["a", "b", "c"];
}
Promotion rules are conservative to avoid unexpected behavior.
Understanding when evaluation happens:
const fn expensive_computation(n: usize) -> usize {
let mut result = 0;
let mut i = 0;
while i < n {
result += i * i;
i += 1;
}
result
}
// Compile-time: computed during compilation
const COMPILE_TIME: usize = expensive_computation(1000);
// Runtime: computed during execution
fn runtime_example(n: usize) {
let runtime_result = expensive_computation(n);
}
// Mixed: some at compile time, some at runtime
const PARTIAL: [usize; 3] = [
expensive_computation(10), // Compile-time
expensive_computation(20), // Compile-time
expensive_computation(30), // Compile-time
];
fn mixed_example(i: usize) {
// Array access at runtime, but values computed at compile time
let value = PARTIAL[i];
}
Key insight: const fn can be called in both const and runtime contexts. The compiler decides when to use CTFE based on the call site.
Const evaluation trades compile time and binary size for runtime performance:
// Large lookup table generated at compile time
const LARGE_TABLE: [u32; 65536] = generate_large_table();
// Embedded directly in binary: adds 256KB to executable
Binary size considerations:
#[cfg(feature = "small-binary")]
const CRC_TABLE: [u32; 16] = small_crc_table(); // Smaller but slower
#[cfg(not(feature = "small-binary"))]
const CRC_TABLE: [u32; 256] = full_crc_table(); // Larger but faster
---
// CRC, hash functions, trigonometry tables
const CRC_TABLE: [u32; 256] = generate_crc_table();
const _: () = {
assert!(CONFIG_SIZE.is_power_of_two());
assert!(BUFFER_ADDR % PAGE_SIZE == 0);
};
const AES_SBOX: [u8; 256] = generate_aes_sbox();
struct Buffer<const SIZE: usize, const ALIGN: usize> { ... }
const GPIO_CONFIG: [PinConfig; 32] = generate_gpio_config();
// BAD: Can't use const evaluation
const fn process_user_input(input: &str) -> Result<Data, Error> {
// input is only known at runtime
}
If data comes from users, files, or network, it can't be const.
2. Complex I/O or System Calls// IMPOSSIBLE: I/O not allowed in const fn
const FILE_CONTENTS: &str = std::fs::read_to_string("config.txt")?;
Const functions can't perform I/O.
3. When Compile Time Matters More Than Runtime// Expensive const evaluation slows compilation
const HUGE_TABLE: [u64; 1_000_000] = generate_huge_table();
Balance compile-time cost against runtime savings.
4. Random or Non-Deterministic Data// IMPOSSIBLE: Non-deterministic operations
const RANDOM_KEY: [u8; 32] = generate_random_key();
Const evaluation must be deterministic.
5. When Binary Size Is Critical// Adds megabytes to binary
const LARGE_DATASET: [f64; 1_000_000] = load_dataset();
Consider runtime generation if binary size is constrained.
---
// ANTI-PATTERN: Unnecessary const complexity
const fn complex_hash(data: &[u8]) -> u64 {
// Hundreds of lines of const-compatible code
// when a simple runtime hash would work fine
}
const HASH1: u64 = complex_hash(b"data1");
const HASH2: u64 = complex_hash(b"data2");
// ... hundreds of const hash calls
Better approach:
// Just use runtime computation
fn hash(data: &[u8]) -> u64 {
// Use optimized runtime hasher
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
hasher.finish()
}
When to const: Only when you're computing the same value repeatedly at runtime or when initialization cost matters.
// ANTI-PATTERN: Expensive computation at compile time
const fn fibonacci_slow(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
n => fibonacci_slow(n - 1) + fibonacci_slow(n - 2), // Exponential!
}
}
// This can take minutes to compile!
const FIB_50: u64 = fibonacci_slow(50);
Better approach:
// Use efficient const algorithm
const fn fibonacci(n: u32) -> u64 {
let mut a = 0u64;
let mut b = 1u64;
let mut i = 0;
while i < n {
let tmp = a + b;
a = b;
b = tmp;
i += 1;
}
a
}
const FIB_50: u64 = fibonacci(50); // Compiles instantly
Rule: Const functions should have reasonable complexity. If it's slow at runtime, it's slow at compile time.
// ANTI-PATTERN: Heap allocation for fixed-size data
struct Buffer {
data: Vec<u8>, // Heap allocated
}
impl Buffer {
fn new(size: usize) -> Self {
Self {
data: vec![0; size], // Runtime allocation
}
}
}
Better approach:
// Use const generics for compile-time size
struct Buffer<const SIZE: usize> {
data: [u8; SIZE], // Stack allocated
}
impl<const SIZE: usize> Buffer<SIZE> {
const fn new() -> Self {
Self {
data: [0; SIZE], // No allocation
}
}
}
When to heap: Only when size is truly dynamic or very large.
// ANTI-PATTERN: Recursive depth limit
const fn deep_recursion(n: u32) -> u32 {
if n == 0 {
0
} else {
1 + deep_recursion(n - 1) // Hits recursion limit
}
}
// Compile error: const evaluation limit exceeded
const RESULT: u32 = deep_recursion(10000);
Better approach:
// Use iteration instead
const fn iterative_count(n: u32) -> u32 {
let mut count = 0;
let mut i = 0;
while i < n {
count += 1;
i += 1;
}
count
}
const RESULT: u32 = iterative_count(10000); // Works fine
CTFE limits (as of Rust 1.82):
#![const_eval_limit = "..."]// ANTI-PATTERN: Runtime-checkable constraint with const panic
const fn create_buffer<const SIZE: usize>() -> [u8; SIZE] {
assert!(SIZE <= 1024, "buffer too large");
[0; SIZE]
}
// Compile error, but could be better
const BUF: [u8; 2048] = create_buffer::<2048>();
Better approach:
// Use type system to enforce constraints
trait ValidSize {}
impl ValidSize for [u8; 256] {}
impl ValidSize for [u8; 512] {}
impl ValidSize for [u8; 1024] {}
struct Buffer<const SIZE: usize>
where
[u8; SIZE]: ValidSize,
{
data: [u8; SIZE],
}
// Won't compile: constraint violated at type level
// let buf: Buffer<2048> = Buffer { data: [0; 2048] };
When to panic: When the constraint is complex and type-level encoding is impractical.
---
The fundamental promise of const evaluation:
// Runtime version
fn crc32_runtime_table() -> [u32; 256] {
let mut table = [0u32; 256];
for i in 0..256 {
let mut crc = i as u32;
for _ in 0..8 {
if crc & 1 == 1 {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
table[i] = crc;
}
table
}
// Const version
const CRC_TABLE: [u32; 256] = generate_crc_table();
// Benchmark comparison
use std::time::Instant;
use std::hint::black_box;
fn bench_runtime_init() {
let start = Instant::now();
let table = crc32_runtime_table(); // ~5 microseconds
black_box(table);
println!("Runtime init: {:?}", start.elapsed());
}
fn bench_const_init() {
let start = Instant::now();
let table = CRC_TABLE; // ~0 nanoseconds (just loads address)
black_box(table);
println!("Const init: {:?}", start.elapsed());
}
Results:
Const evaluation moves work from runtime to compile time:
// Simple const - negligible compile time
const SIMPLE: u32 = 42;
// Moderate const - milliseconds
const MODERATE: [u32; 256] = generate_crc_table();
// Complex const - seconds
const COMPLEX: [u64; 10000] = generate_complex_table();
Measurement:
# Measure compile time impact
cargo clean
time cargo build --release # Without const evaluation
# ~10 seconds
time cargo build --release # With moderate const evaluation
# ~10.5 seconds (+5%)
time cargo build --release # With complex const evaluation
# ~15 seconds (+50%)
Trade-off: Every millisecond added to compile time saves microseconds on every program execution. For long-running services, this pays off immediately.
Const values are embedded in the binary:
// Small const - negligible size
const SMALL: [u8; 16] = [0; 16]; // +16 bytes
// Medium const - moderate size
const MEDIUM: [u32; 256] = generate_crc_table(); // +1 KB
// Large const - significant size
const LARGE: [u64; 65536] = generate_large_table(); // +512 KB
Measurement:
# Check binary size
cargo build --release
ls -lh target/release/myapp
# Without LARGE const: 2.1 MB
# With LARGE const: 2.6 MB (+500 KB)
Mitigation strategies:
For applications with tight startup requirements:
// Embedded system startup
fn main() {
// WITHOUT const evaluation
let crc_table = generate_crc_table(); // +5 µs
let aes_sbox = generate_aes_sbox(); // +10 µs
let lookup = generate_lookup(); // +20 µs
// Total startup overhead: 35 µs
// WITH const evaluation
// All tables already in memory: 0 µs
run_application();
}
Impact: Critical for:
Real-world benchmark of CRC table initialization:
use std::time::Instant;
fn benchmark_initialization() {
const ITERATIONS: usize = 1_000_000;
// Runtime initialization
let start = Instant::now();
for _ in 0..ITERATIONS {
let table = runtime_crc_table();
std::hint::black_box(table);
}
let runtime_duration = start.elapsed();
// Const initialization (just load address)
let start = Instant::now();
for _ in 0..ITERATIONS {
let table = &CRC_TABLE;
std::hint::black_box(table);
}
let const_duration = start.elapsed();
println!("Runtime: {:?} ({} ns per iteration)",
runtime_duration,
runtime_duration.as_nanos() / ITERATIONS as u128);
println!("Const: {:?} ({} ns per iteration)",
const_duration,
const_duration.as_nanos() / ITERATIONS as u128);
println!("Speedup: {}x",
runtime_duration.as_nanos() / const_duration.as_nanos());
}
fn runtime_crc_table() -> [u32; 256] {
let mut table = [0u32; 256];
for i in 0..256 {
let mut crc = i as u32;
for _ in 0..8 {
if crc & 1 == 1 {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
table[i] = crc;
}
table
}
Typical results (AMD Ryzen 9, -O3):
Runtime: 5.234s (5234 ns per iteration)
Const: 0.001s (1 ns per iteration)
Speedup: 5234x
Key insight: The const version is essentially free—it's just loading a pointer. The runtime version does real work every time.
---
Implement const functions for factorial and Fibonacci, then verify they work at compile time.
// TODO: Implement const factorial
const fn factorial(n: u32) -> u64 {
// Your implementation here
}
// TODO: Implement const fibonacci (use iterative approach)
const fn fibonacci(n: u32) -> u64 {
// Your implementation here
}
// Test at compile time
const FACT_10: u64 = factorial(10);
const FIB_20: u64 = fibonacci(20);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_factorial() {
assert_eq!(FACT_10, 3_628_800);
assert_eq!(factorial(5), 120);
}
#[test]
fn test_fibonacci() {
assert_eq!(FIB_20, 6_765);
assert_eq!(fibonacci(10), 55);
}
}
Bonus challenges:
Build a compile-time perfect hash function for a known set of strings.
/// Compute hash at compile time
const fn const_hash(s: &str) -> u32 {
// TODO: Implement simple hash function
// Suggestion: djb2 or FNV-1a
}
/// Perfect hash table for known set of strings
struct PerfectHashMap<const N: usize> {
keys: [&'static str; N],
values: [u32; N],
hashes: [u32; N],
}
impl<const N: usize> PerfectHashMap<N> {
/// Create perfect hash map at compile time
const fn new(entries: [(&'static str, u32); N]) -> Self {
// TODO: Implement
// 1. Extract keys and values
// 2. Compute hashes at compile time
// 3. Verify no collisions
}
/// Lookup with compile-time generated hash
fn get(&self, key: &str) -> Option<u32> {
// TODO: Implement lookup using const_hash
}
}
// Test with HTTP status codes
const STATUS_CODES: PerfectHashMap<5> = PerfectHashMap::new([
("OK", 200),
("NOT_FOUND", 404),
("INTERNAL_ERROR", 500),
("BAD_REQUEST", 400),
("UNAUTHORIZED", 401),
]);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perfect_hash() {
assert_eq!(STATUS_CODES.get("OK"), Some(200));
assert_eq!(STATUS_CODES.get("NOT_FOUND"), Some(404));
assert_eq!(STATUS_CODES.get("UNKNOWN"), None);
}
}
Bonus challenges:
Implement a complete matrix library with const generics, including compile-time identity matrix, transpose, and dimension-checked operations.
use std::ops::{Add, Sub, Mul};
/// Matrix with compile-time dimensions
#[derive(Debug, Clone, Copy, PartialEq)]
struct Matrix<T, const ROWS: usize, const COLS: usize> {
data: [[T; COLS]; ROWS],
}
impl<T, const ROWS: usize, const COLS: usize> Matrix<T, ROWS, COLS> {
/// Create from 2D array
const fn new(data: [[T; COLS]; ROWS]) -> Self {
Self { data }
}
// TODO: Implement these methods
/// Create zero matrix
const fn zero() -> Self
where
T: Copy + Default;
/// Transpose at compile time (NxM -> MxN)
const fn transpose(self) -> Matrix<T, COLS, ROWS>
where
T: Copy;
/// Get element
const fn get(&self, row: usize, col: usize) -> T
where
T: Copy;
}
impl<T, const N: usize> Matrix<T, N, N> {
/// Create identity matrix at compile time
const fn identity() -> Self
where
T: Copy + Default + ~const From<u8>;
}
// TODO: Implement Add, Sub, Mul with dimension checking
// Test cases
const IDENTITY_3X3: Matrix<f32, 3, 3> = Matrix::identity();
const MAT_2X3: Matrix<i32, 2, 3> = Matrix::new([
[1, 2, 3],
[4, 5, 6],
]);
const MAT_3X2: Matrix<i32, 3, 2> = MAT_2X3.transpose();
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identity() {
assert_eq!(IDENTITY_3X3.get(0, 0), 1.0);
assert_eq!(IDENTITY_3X3.get(1, 1), 1.0);
assert_eq!(IDENTITY_3X3.get(0, 1), 0.0);
}
#[test]
fn test_transpose() {
assert_eq!(MAT_3X2.get(0, 0), 1);
assert_eq!(MAT_3X2.get(1, 0), 2);
assert_eq!(MAT_3X2.get(2, 0), 3);
}
#[test]
fn test_multiplication() {
let result = MAT_2X3 * MAT_3X2; // 2x3 * 3x2 = 2x2
// Verify dimensions and values
}
}
Bonus challenges:
---
Rust's standard library extensively uses const functions:
// String and slice operations
const fn str_len(s: &str) -> usize {
s.len() // const since Rust 1.39
}
// Integer operations
const MAX: u32 = u32::MAX; // const since always
const BITS: u32 = u32::BITS; // const since Rust 1.53
// Option and Result
const fn unwrap_or<T>(opt: Option<T>, default: T) -> T {
match opt {
Some(x) => x,
None => default,
}
}
// Array operations
const ARRAY: [i32; 5] = [1, 2, 3, 4, 5];
const FIRST: i32 = ARRAY[0]; // const indexing
As of Rust 1.82, hundreds of standard library functions are const, including most mathematical operations, string methods, and integer operations.
The const-sha256 crate provides compile-time SHA-256 hashing:
use const_sha256::sha256;
// Hash computed at compile time
const HASH: [u8; 32] = sha256(b"Hello, world!");
// Verify hash at compile time
const _: () = {
const EXPECTED: [u8; 32] = [
0x31, 0x5f, 0x5b, 0xdb, 0x76, 0xd0, 0x78, 0xc4,
// ... rest of expected hash ...
];
assert!(matches_hash(HASH, EXPECTED), "hash mismatch");
};
const fn matches_hash(a: [u8; 32], b: [u8; 32]) -> bool {
let mut i = 0;
while i < 32 {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
Use cases:
The typenum crate provides compile-time arithmetic through types:
use typenum::{U2, U3, Prod, Sum};
// Type-level addition: 2 + 3 = 5
type Five = Sum<U2, U3>;
// Type-level multiplication: 2 * 3 = 6
type Six = Prod<U2, U3>;
// Use in const generics
struct Buffer<N: Unsigned> {
data: [u8; N::USIZE],
}
type SmallBuffer = Buffer<U256>;
type LargeBuffer = Buffer<Prod<U256, U4>>; // 256 * 4 = 1024
Use cases:
generic-array provides fixed-size arrays before const generics were stable (now mostly superseded):
use generic_array::{GenericArray, arr};
use generic_array::typenum::U1024;
// Fixed-size array without heap allocation
type MyArray = GenericArray<u8, U1024>;
const MY_ARRAY: MyArray = arr![u8; 0; 1024];
Modern code should use native const generics, but generic-array still appears in older codebases.
Embedded HAL uses const evaluation for zero-cost pin configuration:
use embedded_hal::digital::v2::OutputPin;
// Pin configuration at compile time
struct Pin<const PIN: u8, const MODE: u8> {
_marker: std::marker::PhantomData<[u8; PIN]>,
}
const OUTPUT: u8 = 1;
const INPUT: u8 = 0;
const GPIO_BASE: *mut u32 = 0x4000_0000 as *mut u32;
impl<const PIN: u8> Pin<PIN, OUTPUT> {
const VALIDATED: () = {
assert!(PIN < 32, "invalid pin number");
};
const fn new() -> Self {
let _ = Self::VALIDATED;
Self { _marker: std::marker::PhantomData }
}
fn set_high(&mut self) {
// Hardware access using const PIN
unsafe {
GPIO_BASE.offset((PIN / 32) as isize)
.write_volatile(1 << (PIN % 32));
}
}
}
// Type-safe, zero-cost pin access
const LED_PIN: Pin<13, OUTPUT> = Pin::new();
const BUTTON_PIN: Pin<7, INPUT> = Pin::new();
Benefits:
---
---
Const evaluation represents the ultimate performance optimization: computation that costs zero at runtime because it happened at compile time. After a millennium of optimizing performance-critical systems, I've learned that the fastest code is code that never runs—because the result is already embedded in your binary.
The beauty of const evaluation lies in its flexibility. A const fn can be called both at compile time (for zero runtime cost) and at runtime (when dealing with dynamic data). The compiler seamlessly chooses the appropriate execution context, giving you the best of both worlds.
From embedded systems with microsecond boot requirements to cryptographic implementations demanding constant-time guarantees, const evaluation is the foundation of zero-cost abstractions. Master it, and you'll write code that runs before your program even starts.
Now go forth and compute at compile time—your runtime will thank you for the cycles you never spent.
Happy const evaluating! 🦀⚡
Run this code in the official Rust Playground