Constant Folding and Propagation
The compiler computing constant expressions at compile time so the program does not have to.
Doing the Math at Compile Time
Some of the arithmetic in your program does not depend on anything that happens while it runs. If you write 7 * 24 * 60 * 60, the answer is the same on every machine, every time. There is no reason to make the CPU compute it when the program runs; the compiler can work it out once, during compilation, and bake the answer straight into the code.
Two closely related optimizations do this:
- Constant folding: evaluating an expression made entirely of constants at compile time.
- Constant propagation: tracking that a variable holds a known constant and substituting that value wherever the variable is used (which then often exposes more folding).
They work hand in hand, and this lesson looks at both.
Folding a Chain of Constants
Here is a function that computes the number of seconds in a week, step by step:
int secondsInWeek()
{
int days{7};
int hours{days * 24};
int minutes{hours * 60};
int seconds{minutes * 60};
return seconds;
}
Every value here is derived from the literal 7. Nothing comes from outside the function. Watch what -O0 does with it:
secondsInWeek():
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], 7
mov edx, DWORD PTR -4[rbp]
mov eax, edx
add eax, eax
add eax, edx
sal eax, 3
mov DWORD PTR -8[rbp], eax
mov eax, DWORD PTR -8[rbp]
imul eax, eax, 60
mov DWORD PTR -12[rbp], eax
mov eax, DWORD PTR -12[rbp]
imul eax, eax, 60
mov DWORD PTR -16[rbp], eax
mov eax, DWORD PTR -16[rbp]
pop rbp
ret
The unoptimized compiler computes the whole thing at run time. It stores 7, multiplies by 24 (notice it does this as add/add/sal eax, 3, a clever way to multiply by 24 = 8 * 3), then two imul eax, eax, 60 instructions, storing each intermediate to its own stack slot. Every multiplication that you wrote actually happens on the CPU.
Now -O2:
secondsInWeek():
mov eax, 604800
ret
That is the entire function. The optimizer folded 7 * 24 * 60 * 60 down to the single constant 604800, loaded it into eax, and returned. Not one multiply survives, because there was nothing to multiply at run time: the answer was always going to be 604800. All of that arithmetic was paid for once, by the compiler, before your program ever ran.
Propagation: Following a Constant Through Variables
Folding handles expressions that are already all constants. Propagation is what lets the compiler discover that a variable is constant in the first place, even when it is mixed with run-time data.
int compute(int x)
{
int a{5};
int b{a + 3};
return x * b;
}
Here x is a genuine run-time argument, so the function cannot be reduced to a single constant. But a is clearly 5, and therefore b is clearly 5 + 3 = 8. A human reads this instantly. So does the optimizer.
At -O0 the compiler plays dumb, storing 5, adding 3, and reading the result back to multiply:
compute(int):
push rbp
mov rbp, rsp
mov DWORD PTR -20[rbp], edi
mov DWORD PTR -4[rbp], 5
mov eax, DWORD PTR -4[rbp]
add eax, 3
mov DWORD PTR -8[rbp], eax
mov eax, DWORD PTR -20[rbp]
imul eax, DWORD PTR -8[rbp]
pop rbp
ret
At -O2 it propagates a = 5 into b, folds b to 8, and substitutes that constant into the multiplication:
compute(int):
lea eax, 0[0+rdi*8]
ret
The function is now just "multiply x by 8". The constants 5 and 3 are gone; the optimizer carried them forward, combined them, and used the result. Even better, it multiplied by 8 not with imul but with lea eax, 0[0+rdi*8], which uses the addressing-mode scale factor to compute rdi * 8 for free. The local variables a and b left no trace whatsoever.
Why This Matters for Reading Assembly
Once you know about folding and propagation, a whole category of "where did my code go?" mysteries dissolves. When you see a bare mov eax, 604800 where your source clearly had a chain of multiplications, you are not looking at a bug or a different function. You are looking at arithmetic the compiler already finished.
This also has a practical consequence for how you write code. Naming intermediate steps with clearly-constant locals, as in secondsInWeek, costs nothing at -O2: those locals vanish and the constant is folded just as if you had written the final number. You get readable source and optimal output at the same time. (In later study you will meet constexpr, which lets you require that a computation happen at compile time rather than merely hoping the optimizer does it.)
A Note on What Stays
Propagation only works when the compiler can prove a value is constant. The moment a value comes from outside, from std::cin, a function argument it cannot see through, or anything decided at run time, it stops being foldable and the real instructions stay. In compute, x survived precisely because it is a genuine argument. The skill is learning to look at a listing and separate the part the compiler could pin down (gone, folded into constants) from the part it genuinely had to compute (still there).
Key Takeaways
- Constant folding evaluates all-constant expressions at compile time, so
7 * 24 * 60 * 60becomes the literal604800. - Constant propagation tracks variables known to hold constants and substitutes their values, often exposing further folding.
- At
-O2, a function whose result is entirely constant can collapse to a singlemovof that constant and aret. - Folded constants leave no trace of the original arithmetic or the named locals that held them.
- The optimizer only folds what it can prove constant; values from
std::cinor opaque arguments stay as real run-time instructions. - Writing readable code with named constant locals costs nothing at
-O2, because those locals are folded away.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Constant Folding and Propagation - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!