Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building a Smaller App Binary

Building a Smaller App Binary

try! Swift 2024 Tokyo https://tryswift.jp/

Yuta Saito

March 23, 2024
Tweet

More Decks by Yuta Saito

Other Decks in Programming

Transcript

  1. • Yuta Saito / @kateinoigakukun • Waseda University M1 •

    Maintainer of SwiftWasm • Commiter to Swift / LLVM / CRuby • Compiler Squad at About me
  2. Outline 1. Motivation: Why binary size is important? 2. Compiler

    Internals: Toolchain Optimizations 3. Pro Tips: How to reduce your App size
  3. Motivation When/Where does App size matter? • 15 MB limit

    for App Clip • 200 MB limit over Cellular Network • iOS 13 or later lifted the restriction, but it’s still opt-in
  4. Motivation When/Where does App size matter? • Web Apps •

    Goodnotes is using Swift with WebAssembly to bring it to Web • Need instant launch-time • Embedded Systems • Limited resources on storage size and memory size
  5. Toolchain Internals: Build Pipeline Swift Code Object File Executable Swift

    Code Swift Code Object File Object File SIL LLVM IR SIL Optimizer LLVM Optimizer Linker Optimizer Swift Compiler Linker
  6. Toolchain Internals • Dead Code Elimination • Dead Function Elimination

    • … Optimizers in Compiler protocol Multipliable { func multiply(by count: Int) -> Self func isMultipleOfTwo() -> Bool } extension Int: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } extension String: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } func optimizeMe<X: Multipliable>(given x: X) -> X { let y = x.multiply(by: 2) externalCitizen() localCitizen() return y } func externalCitizen() -> Int func localCitizen() -> Int { return 42 } public func main(whatever: Int) { optimizeMe(given: whatever) }
  7. Toolchain Internals • Dead Code Elimination • Dead Function Elimination

    • … Optimizers in Compiler Can remove operations when • The result of the operation is not used • No side e ff ect protocol Multipliable { func multiply(by count: Int) -> Self func isMultipleOfTwo() -> Bool } extension Int: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } extension String: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } func optimizeMe<X: Multipliable>(given x: X) -> X { let y = x.multiply(by: 2) externalCitizen() localCitizen() return y } func externalCitizen() -> Int // may have side effect func localCitizen() -> Int { return 42 } // no side effect public func main(whatever: Int) { optimizeMe(given: whatever) }
  8. Toolchain Internals • Dead Code Elimination • Dead Function Elimination

    • … Optimizers in Compiler Can remove operations when • The result of the operation is not used • No side e ff ect protocol Multipliable { func multiply(by count: Int) -> Self func isMultipleOfTwo() -> Bool } extension Int: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } extension String: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } func optimizeMe<X: Multipliable>(given x: X) -> X { let y = x.multiply(by: 2) externalCitizen() localCitizen() return y } func externalCitizen() -> Int // may have side effect func localCitizen() -> Int { return 42 } // no side effect public func main(whatever: Int) { optimizeMe(given: whatever) } Useful
  9. Toolchain Internals • Dead Code Elimination • Dead Function Elimination

    • … Optimizers in Compiler Can remove operations when • The result of the operation is not used • No side e ff ect protocol Multipliable { func multiply(by count: Int) -> Self func isMultipleOfTwo() -> Bool } extension Int: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } extension String: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } func optimizeMe<X: Multipliable>(given x: X) -> X { let y = x.multiply(by: 2) externalCitizen() localCitizen() return y } func externalCitizen() -> Int // may have side effect func localCitizen() -> Int { return 42 } // no side effect public func main(whatever: Int) { optimizeMe(given: whatever) } Useful Useful
  10. Toolchain Internals • Dead Code Elimination • Dead Function Elimination

    • … Optimizers in Compiler Can remove operations when • The result of the operation is not used • No side e ff ect protocol Multipliable { func multiply(by count: Int) -> Self func isMultipleOfTwo() -> Bool } extension Int: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } extension String: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } func optimizeMe<X: Multipliable>(given x: X) -> X { let y = x.multiply(by: 2) externalCitizen() localCitizen() return y } func externalCitizen() -> Int // may have side effect func localCitizen() -> Int { return 42 } // no side effect public func main(whatever: Int) { optimizeMe(given: whatever) } Useful Maybe Useful Useful
  11. Toolchain Internals • Dead Code Elimination • Dead Function Elimination

    • … Optimizers in Compiler Can remove operations when • The result of the operation is not used • No side e ff ect protocol Multipliable { func multiply(by count: Int) -> Self func isMultipleOfTwo() -> Bool } extension Int: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } extension String: Multipliable { func multiply(by count: Int) -> String { /* ... */ } func isMultipleOfTwo() -> Bool { /* ... */ } } func optimizeMe<X: Multipliable>(given x: X) -> X { let y = x.multiply(by: 2) externalCitizen() localCitizen() return y } func externalCitizen() -> Int // may have side effect func localCitizen() -> Int { return 42 } // no side effect public func main(whatever: Int) { optimizeMe(given: whatever) } Useful Dead Maybe Useful Useful
  12. Toolchain Internals Optimizers in Compiler Can remove symbols when •

    The symbol is unreachale from any code path reachable from external • The compiler knows all possible use of the symbol protocol Multipliable { func multiply(by count: Int) -> Self // func isMultipleOfTwo() -> Bool } extension Int: Multipliable { func multiply(by count: Int) -> String { /* ... */ } // func isMultipleOfTwo() -> Bool { /* ... */ } } extension String: Multipliable { func multiply(by count: Int) -> String { /* ... */ } // func isMultipleOfTwo() -> Bool { /* ... */ } } func optimizeMe<X: Multipliable>(given x: X) -> X { let y = x.multiply(by: 2) externalCitizen() // localCitizen() return y } func externalCitizen() -> Int // func localCitizen() -> Int { return 42 } public func main(whatever: Int) { optimizeMe(given: whatever) } • Dead Code Elimination • Dead Function Elimination • …
  13. Toolchain Internals • GC not-referenced functions / data • Linker

    options • ld64: -dead_strip • gold/lld: --gc-sections • Even public code can be eliminated if it’s unreachable from entry point or exported symbols • But cannot understand VTable / Witness Table entries, so they are always marked live conservatively Linker GC
  14. Toolchain Internals • GC not-referenced functions / data • Linker

    options • ld64: -dead_strip • gold/lld: --gc-sections • Even public code can be eliminated if it’s unreachable from entry point or exported symbols • But cannot understand VTable / Witness Table entries, so they are always marked live conservatively Linker GC --gc-sections recently started working with Swift and WebAssembly!
  15. Don't guess, measure! Linkmap gives you some insights (ld -map

    link.map) Find the size bottleneck # Path: Debug/MyApp.app/MyApp # Arch: arm64 # Object files: [ 0] linker synthesized [ 1] objc-file [ 2] arm64/ContentView.o [ 3] arm64/MyMacAppApp.o [ 4] arm64/GeneratedAssetSymbols.o ... # Sections: # Address Size Segment Section 0x1000038D8 0x000039D0 __TEXT __text 0x1000072A8 0x00000288 __TEXT __stubs 0x100007530 0x0000025E __TEXT __swift5_typeref 0x100007790 0x000000F7 __TEXT __cstring ... # Symbols: Size File Name 0x000001C4 [ 2] _$s5MyApp11ContentViewV4b 0x0000006C [ 2] ___swift_instantiateConcr 0x0000051C [ 2] _$s5MyApp11ContentViewV4b 0x00000014 [ 2] _$s7SwiftUI10ShapeStylePA 0x00000044 [ 2] _$s7SwiftUI11ViewBuilderV 0x00000238 [ 2] _$s7SwiftUI11ViewBuilderV ...
  16. The first step: Reduce source code • Remove unused code

    • Periphery • Optimizers can remove them, but still better to remove by yourself • Reduce number of dependency libraries • Prefer system installed libraries
  17. Internalize Protocols Public protocols prevent Dead Function Elimination against witness

    methods public protocol MyProtocol { func foo() func bar() } struct A: MyProtocol { func foo() {} func bar() {} } struct B: MyProtocol { func foo() {} func bar() {} } public protocol MyProtocol { func foo() } protocol InternalMyProtocol { func bar() } struct A: MyProtocol, InternalMyProtocol { func foo() {} func bar() {} } struct B: MyProtocol, InternalMyProtocol { func foo() {} func bar() {} }
  18. Reduce virtual function calls • A virtual function call marks

    all possible callee methods live • Never called virtual method slots can be removed protocol MyProtocol { func foo() func bar() } struct A: MyProtocol { func foo() {} func bar() {} } struct B: MyProtocol { func foo() {} func bar() {} } func optimizeMe<X: MyProtocol>(_ x: X) { x.foo() x.bar() } protocol MyProtocol { func foo() func bar() } struct A: MyProtocol { func foo() {} func bar() {} } struct B: MyProtocol { func foo() {} func bar() {} } func optimizeMe<X: MyProtocol>(_ x: X) { x.foo() x.bar() }
  19. Experimental: Link-time Optimization • Linker knows all inputs → Can

    prove more unreachable code • Public symbols can be internalized automatically • Optimize at LLVM IR level to know VTable/Witness Table structure • --experimental-hermetic-seal-at-link Object File Executable Object File Object File Linker GC LLVM LTO Object File Linker
  20. Experimental: Embedded Swift 🥗 Extreme mode for expert developers •

    Minimum subset of runtime library • All generic functions are specialized • All modules are linked at SIL level, compiled into a single object le Swift Code Object File Swift Code Swift Code SIL LLVM IR SIL Optimizer LLVM Optimizer Swift Compiler
  21. Experimental: Embedded Swift 🥗 Extreme mode for expert developers •

    Minimum subset of runtime library • All generic functions are specialized • All modules are linked at SIL level, compiled into a single object le Swift Code Object File Swift Code Swift Code SIL LLVM IR SIL Optimizer LLVM Optimizer Swift Compiler Calls through protocols are always direct call!
  22. Summary • How Swift toolchain optimizes your App size •

    Tame the optimizer with some hints • Possible future visions: Link-time Optimization, Embedded Swift