Const in the Compiler
Introduction
The compiler's job is to eliminate redundant work. To do that, it needs to know what can't change. For local variables and simple expressions, it figures this out on its own. The place it genuinely needs your help is across function call boundaries — any call to an external function could theoretically modify a global, so the compiler has to assume the worst and reload.
const is the promise that removes that assumption. Not because it enforces anything at runtime, but because it gives the compiler information it couldn't derive itself. That's when you see it actually matter in the output.
Basic Example
// mutable global - compiler must reload from memory every call
int factor = 10;
int scale(int x) {
return x * factor;
}
// const global - compiler bakes the value in directly
const int FACTOR = 10;
int scale_const(int x) {
return x * FACTOR;
}
With a mutable global, the compiler has to assume something else could modify factor between calls, so it emits a memory load every time. With const, it knows the value at compile time and never touches memory. The assembly makes this obvious:
scale(int):
mov eax, DWORD PTR factor[rip] ; load factor from memory
imul eax, edi
ret
scale_const(int):
lea eax, [rdi+rdi*4] ; 5*x using address hardware
add eax, eax ; double it -> 10*x, no memory access
ret
factor:
.long 10
There's a bonus: the const version doesn't even use imul. The compiler replaces multiplication by 10 with a lea (computes 5x using address calculation hardware) followed by an add to double it — strength reduction that's faster than a multiply.
Across Function Call Boundaries
int mutable_factor = 10;
const int const_factor = 10;
__attribute__((noinline)) void side_effect() {
mutable_factor = 20;
}
int scale_mutable(int x) {
int a = x * mutable_factor;
side_effect(); // mutable_factor might have changed
int b = x * mutable_factor; // compiler must reload
return a + b;
}
int scale_const(int x) {
int a = x * const_factor;
side_effect(); // const_factor cannot have changed
int b = x * const_factor; // compiler skips the reload
return a + b;
}
scale_mutable(int):
mov eax, DWORD PTR mutable_factor[rip] ; first load
call side_effect()
imul eax, edi
imul edi, DWORD PTR mutable_factor[rip] ; forced reload after call
add eax, edi
ret
scale_const(int):
call side_effect()
lea eax, [rdi+rdi*4] ; no reload needed
sal eax, 2 ; = 20x (both multiplications folded)
ret
main:
...
mov esi, 100 ; scale_const(5) computed at compile time
After side_effect(), the mutable version reloads mutable_factor from memory — it has to, since the function could have changed it. The const version doesn't bother. In main, scale_const(5) compiles down to mov esi, 100 — the entire computation was done at compile time.
The mut Keyword in Rust
Rust makes this tradeoff explicit by design. Every variable is immutable by default — you have to opt into mutability with mut. I originally interpreted it while learning Rust as a choice made to annoy C++ devs into writing safe code. For the lazy dev, you're now "forced beyond the pale" to declare your variables mutable intentionally.
fn main() {
let normal_var:i32 = 20;
let mut var_assigned_mut:i32 = 20;
// this won't compile
normal_var += 12
// clearly this will
var_assigned_mut += 12
}
Intentionality is the key distinguisher on the qualitative value of a programming language, and it's clear in this case the language is right in helping the programmer think harder about which variables need mutability. This has one benefit of managing side effects, but the deeper payoff is that pushing code toward more const declarations gives the compiler exactly the information it needs to do its job.