Share
Embed
Transcript
30 slides
-
• Yuta Saito / @kateinoigakukun • Waseda University M1 •
Maintainer of SwiftWasm • Commiter to Swift / LLVM / CRuby • Compiler Squad at About me -
Outline 1. Motivation: Why binary size is important? 2. Compiler
Internals: Toolchain Optimizations 3. Pro Tips: How to reduce your App size -
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 -
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 -
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 -
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) } -
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) } -
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 -
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 -
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 -
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 -
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 • … -
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 -
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! -
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 ... -
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 -
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() {} } -
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() } -
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 -
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 -
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! -
Summary • How Swift toolchain optimizes your App size •
Tame the optimizer with some hints • Possible future visions: Link-time Optimization, Embedded Swift