Slide 1

Slide 1 text

Yuta Saito (@kateinoigakukun) 2024/03/23 Building a Smaller App Binary try! Swift 2024 Tokyo

Slide 2

Slide 2 text

• Yuta Saito / @kateinoigakukun • Waseda University M1 • Maintainer of SwiftWasm • Commiter to Swift / LLVM / CRuby • Compiler Squad at About me

Slide 3

Slide 3 text

Outline 1. Motivation: Why binary size is important? 2. Compiler Internals: Toolchain Optimizations 3. Pro Tips: How to reduce your App size

Slide 4

Slide 4 text

1. Motivation:
 Why is App size important for us?

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

2. Toolchain Internals: Size Optimizations

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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(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) }

Slide 10

Slide 10 text

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(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) }

Slide 11

Slide 11 text

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(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

Slide 12

Slide 12 text

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(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

Slide 13

Slide 13 text

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(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

Slide 14

Slide 14 text

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(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

Slide 15

Slide 15 text

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(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 • …

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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!

Slide 18

Slide 18 text

3. Pro Tips: Writing size-friendly Swift code

Slide 19

Slide 19 text

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 ...

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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() {} }

Slide 22

Slide 22 text

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: 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: X) { x.foo() x.bar() }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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!

Slide 26

Slide 26 text

Live Demo Swift on Playdate

Slide 27

Slide 27 text

github.com/kateinoigakukun/swift-playdate Live Demo Only 63KB!

Slide 28

Slide 28 text

Live Demo

Slide 29

Slide 29 text

Live Demo

Slide 30

Slide 30 text

Summary • How Swift toolchain optimizes your App size • Tame the optimizer with some hints • Possible future visions: Link-time Optimization, Embedded Swift