Compiler optimizations

The code compiled by Inko is optimized in two ways: the mid-level IR ("MIR"), generated after type checking, is optimized in various ways, and various LLVM optimizations passes are applied when generating machine code.

When using the default optimization level balanced, the only LLVM optimization pass that's run is the mem2reg pass. We plan to run more passes once we have a better understanding of which ones are relevant for Inko. For more details, refer to this issue.

Type specialization

The first optimization is type specialization, and this optimization is always enabled as it's required for generating correct code. This pass takes generic types and methods and generates versions specialized to the types they are used with. Specialization is performed over shapes instead of types.

A shape is essentially a "bucket" of different types that have the same memory layout and/or aliasing semantics. For example, Int and Float have their own shapes, while all owned values allocated on the heap (by default) share the same Owned shape. This grouping allows for a better balance between fast compile times and efficient runtime performance.

You can find more details about how this works in the design guide about generics.

Inlining

Inko's compiler is able to inline static and instance method calls, provided they're called using static dispatch. async methods are never inlined as they are executed asynchronously.

Methods annotated with the inline keyword are always inlined (provided inlining is enabled in the first place). This means you can use this keyword to forcefully inline a method, such as is the case for methods such as Int.+ and Float.+.

Methods without the inline keyword are only inlined if the compiler deems this beneficial. This works by giving each method a score (known as a "weight") based on its number of instructions. The greater the weight, the larger the method. To inline methods, the inliner builds a graph of all methods called and processes them in bottom-up order. For each method (= the "caller"), the inliner looks at each called method (= the "callee"). If the combined size of the caller and callee is below the inlining threshold, the callee is inlined into the caller. The inliner might also inline a method if it's called in only a few places, even if doing so would exceed the caller's inline threshold.

The algorithm used for calculating the method weights and the inlining threshold are unspecified, can't be configured by the user, and are subject to change.

Dead code removal

It's possible some methods are no longer called if the inliner inlined them into all their call sites. The compiler detects such methods and removes them, reducing the size of the final executable.

Methods that might be the target of a dynamic dispatch call site are only removed if the compiler can statically determine they're never called. This means that methods defined through traits might not be removed, even if they're never called.

The compiler also detects and removes instructions that don't have any side effects and of which the result is unused. This is limited to simple instructions such as those used for integer and string literals, allocations, and a few others.