An optimizer “improves” the IR, but that can mean a lot of different things. Improve could mean “run faster” or “use less memory”. Or perhaps you want to optimize for memory access time because CPUs are so fast it is sometimes more efficient to repeatedly calculate something rather than calculate it once, store it and access it later.
The Compiler series:
- Compilers 101 – Overview and Lexer
- Compilers 102 – Parser
- Compilers 103 – Semantic Analyzer
- Compilers 104 – IR Generation
- Compilers 105 – Back End Overview
The Optimizer does a series of transformations to the IR code, typically in multiple passes. LLVM provides full control over these passes.
Not all LLVM optimizations provide benefits to Xojo code. We have distilled the many complicated optimization settings that are available with LLVM into three options that are useful for Xojo code, which you can set from the Shared Build settings: Default, Moderate and Aggressive.
The Default optimization does minimal optimization in order to have the quickest compile times.
The Moderate setting does more optimizations, primarily to reduce the time needed for mathematical calculations which results in slightly slower compile times.
The Aggressive setting does many more mathematical optimizations to further reduce calculation time, but also dramatically increases compile time.
Optimization intends to create something that is equivalent to the original code. The end result is the same, even if the means to do so might be very different. For example, the optimizer may determine that it should do a bit shift to do integer math as a single operation rather than a series of add operations. This could result in smaller, faster code but may take longer for the optimizer to process the code in order to make this determination.
Deciding the “best” optimization for any code is not technically a solvable problem (np-hard), so optimizers use a combination of “heuristics and hand-waving”. “Hand-waving” means the compiler thinks this is correct, but has no real way to prove it. And heuristics simply means that optimizations that have been known to work in prior code are used when similar code is found.
Constant Folding is a simple example of an optimization that can be done.
This essentially mans the optimizer evaluates constant expressions up front to reduce execution time, and save stack and register space.
a = 1 + 2
can be replaced with:
a = 3
Not everything will be quite so obvious in your code, of course.
There are certain types of code that can make the optimizer’s job more difficult. These special Xojo features can challenge an optimizer:
- Exception Handling: Makes things difficult because you cannot tell where a function call may return.
- Memory Management: Xojo has deterministic object destruction when variable goes out of scope, which forces the optimizer to have to track things more closely.
- Introspection: The use of Introspection requires lots of metadata to remain available so it can be referenced at run-time.
- Threading: Cooperative thread yielding affects loops and other things. Xojo is a “safe” language and many of the things it does to ensure your app does not crash (and instead raise exceptions) such as Nil object checks, stack overflow checks, bounds checks, etc. all restrict what the optimizer can do.
- Plugins: Since these are pre-compiled, they are ignored by the optimizer.
In part 7 I will cover how the optimizer can unroll loops. Stay tuned.