February 8, 2018·5 min read·engineeringswiftcompilersoptimization

Swift's size optimization changes the cost of inlining

Swift 4.1 adds a size-oriented optimization mode. Smaller inlining thresholds and shared helper functions reduce duplication while preserving optimizations that can remove code.

Inlining a function can make a program larger, but refusing to inline it can also waste space. A call to a small constant-returning getter may require more machinery than its body. Exposing that constant to the caller can then eliminate a branch. Swift's new size optimization mode has to account for both effects, which makes its design more interesting than a switch that disables aggressive compilation.

Erik Eckstein's February 8 introduction to code size optimization describes the new -Osize mode in Swift 4.1. The compiler still optimizes the program. Its decisions give more weight to the amount of generated code, including a lower size threshold for inlining. This gives application authors an explicit choice about the compiler's objective.

I like Swift's value types and its generally deliberate language design. Compiler policy matters to that appeal because useful source abstractions still need a reasonable implementation. A size objective gives the compiler another way to handle their cost without asking an application author to flatten every abstraction by hand.

An inline expansion can remove work

The announcement uses a getter returning a constant to explain why disabling inlining would be the wrong implementation. The following small example uses the same mechanism, with an observable branch body.

struct ConstantValue {
    var value: Int { return 27 }
}

func report(_ item: ConstantValue) {
    if item.value != 27 {
        print("unexpected")
    }
}

If the compiler replaces the property access with the getter's body, it can see that the condition is false. The branch and its print call can disappear. The important optimization is the information exposed at the call site. Removing the call is only the first step.

A general rule against inlining would prevent that simplification. It would also retain call overhead for getters whose implementations are smaller than the instructions required to call them. The size mode therefore keeps inlining available and changes its threshold. That leaves room for small expansions that reduce the final program.

The opposite case explains the lower threshold. Copying a larger function body into several callers can duplicate instructions. A speed-oriented optimizer may accept that cost because the expanded code enables faster execution. A size-oriented optimizer has a stronger reason to keep a shared implementation. The announcement describes this as avoiding duplication, while recognizing that some inlining remains useful.

The resulting distinction is local to the optimizer's choices. Selecting -Osize does not promise that every function stays out of line. A getter can still disappear into its caller, and another function can remain a call. Reading the setting as a blanket prohibition would misrepresent both the example and the stated design.

Shared helpers reduce repeated implementation code

Inlining is one part of the change. The announcement also says that certain patterns for generic types and Objective-C bridging are extracted into helper functions instead of being generated inline. Those repeated patterns are another source of code size. A shared helper allows several occurrences to use one implementation.

This is relevant to abstractions whose source representation looks small. A generic operation describes work without fixing its concrete type in the source. Bridging also expresses behavior that the implementation must supply. The amount of text in the Swift file does not establish how much machine code these operations require. Factoring repeated implementation patterns can reduce code without changing the application's declarations.

The tradeoff follows from the same mechanism. Sharing implementation code can require calls where an inline sequence previously existed. Whether that changes application performance materially depends on which paths execute and how often. The compiler team reports that the performance effect is negligible for most applications in its observations, while explicitly allowing that performance-sensitive code may favor -O.

The reported size reductions range between five and thirty percent for some projects. Those are observations from the announcement, not a reduction guaranteed for another application. The report also describes performance losses below five percent for most applications. Both claims concern the team's observed projects, and the article does not supply a universal relationship between bytes removed and runtime cost.

Compilation scope is a separate choice

The earlier explanation of whole-module optimization, published in October 2016, supplies useful context. In single-file compilation, optimization has limited visibility into implementations in other files. A caller can know a generic function's declaration without seeing enough of its implementation to specialize and inline it.

Whole-module optimization lets the compiler consider all the module's files together. The article demonstrates a generic container whose getter can specialize for Int. Once the implementation is visible, the compiler can reduce a pair of getter calls to accesses to the stored elements. The gain depends on visibility of the concrete use and the function body.

That broader view can also remove unused functions. After callers absorb an implementation, the original function may have no remaining uses. The compiler can remove a non-public function when it can establish that it is unused within the module. This is another reason that optimizing for size does not imply minimizing every intermediate expansion.

Swift 4.1 makes compilation mode independently selectable from optimization mode. The new size objective works with single-file and whole-module compilation. According to the announcement, whole-module compilation gives the best optimization results. The two settings answer different questions about how much code the optimizer sees and which result it favors.

Selecting the objective

On the command line, -Osize replaces -O. A small program in main.swift can use the size mode through the compiler invocation below.

swiftc -Osize main.swift -o example

The announcement also documents a Swift compiler code-generation setting in Xcode 9.3. Its separate compilation-mode setting means selecting size optimization does not silently answer the whole-module question. Build configuration can express both decisions.

My least pleasant part of Swift development is build time, so I care about that separation. The 2016 article explains that whole-module compilation performs Swift-specific optimization together, then splits the module for backend work. It describes parallel backend processing and reuse of unchanged parts. Whole-module compilation has a more specific implementation than the assumption that every stage becomes a single indivisible job.

The new flag should therefore be understood as an optimization policy for the generated program. Its documented mechanisms include a smaller inlining threshold and helper extraction for repeated patterns. Applications that can accept the stated runtime tradeoff gain a compiler-supported way to reduce instruction duplication while retaining the simplifications that make small abstractions economical.