The compiler has a lot more freedom when dealing with constants because of the following:
- The value does not change.
- The type of the constant does not change.
This will become clear after we look into some simple examples.
Let's take a look at the following function:
function constant_folding_example()
a = 2 * 3
b = a + 1
return b > 1 ? 10 : 20
end
If we just follow the logic, then it is not difficult to see that it always returns a value of 10. Let's just unroll it quickly here:
- The a variable has a value of 6.
- The b variable has a value of a + 1, which is 7.
- Because the b variable is greater than 1, it returns 10.
From the compiler's perspective, the a variable can be inferred as a constant because it is assigned but never changed, and likewise for the b variable.
We can take a look at the code generated by Julia for this:
The Julia compiler goes through several stages. In this case, we can use the @code_typed macro, which shows the code that has been generated where all type information has been resolved.
Voila! The compiler has figured it all out and just returns a value of 10 for this function.
We realize that a couple of things have happened here:
- When the compiler saw the multiplication of two constant values (2 * 3), it computed the final value of 6 for a. This process is called constant folding.
- When the compiler inferred a as a value of 6, it calculated b as a value of 7. This process is called constant propagation.
- When the compiler inferred b as a value of 7, it pruned away the else-branch from the if-then-else operation. This process is called dead code elimination.
Julia's compiler optimization is truly state of the art. These are just some of the examples that we can get a performance boost automatically without having to refactor a lot of code.