
Swift 컴파일러가 모듈 전체를 하나의 단위로 보고 최적화를 수행하는 기술
하나의 모듈 내의 소스 파일들을 한 번에 분석하여 최적화 하는 기술
다른 파일에 정의된 함수의 코드를 호출 부에 직접 복사하여 함수 호출 오버헤드를 제거
호출된 함수의 주소를 따라가지 않고 바로 그 자리에서 코드 실행
상속되지 않거나 오버라이딩되지 않은 메서드를 파악하여, 동적 디스패치를 정적 디스패치로 변경
ex) 프로토콜을 따르나, 구현체가 1개 밖에 없는 경우
// File 1
public protocol Calculator {
func add(_ a: Int, _ b: Int) -> Int
}
public final class MathProcessor {
private let calculator: Calculator
public init(calculator: Calculator) {
self.calculator = calculator
}
public func process(x: Int, y: Int) -> Int {
// WMO가 없다면 컴파일러는 이 시점에 calculator.add가
// 실제로 어떤 코드를 실행하는지 알기 어려움(동적 디스패치).
return calculator.add(x, y)
}
}
-------------------------------------------------
// File 2
/// Calculator의 실제 구현체
internal final class StandardCalculator: Calculator {
// @inlinable이 없어도 WMO 환경에서는 다른 파일에서 인라인화 가능성이 생김
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
}
WMO가 아닌 환경에서는 컴파일러가 다른 파일에 있는 구현체를 모름.
그러므로, process 메서드 내 calculator.add는 동적 디스패치로 처리됨. (어떤 구현체가 들어올 지 모르기 때문)
WMO 환경에서 컴파일러는 두 파일을 동시에 봄.
들어올 Calulator 구현체가 무조건 1개인 걸 컴파일러가 알게 됨.
컴파일러는 가상 함수 제거를 수행하고, add 메서드 코드를 process 메서드 내로 복사 진행
private / fileprivate 사용으로 파일 내부에서만 타입이 쓰이는 게 100% 보장되면 최적화 가능
public protocol Calculator {
func add(_ a: Int, _ b: Int) -> Int
}
// 구현체 1
final class BasicCalculator: Calculator {
func add(_ a: Int, _ b: Int) -> Int { return a + b }
}
// 구현체 2
final class ScientificCalculator: Calculator {
func add(_ a: Int, _ b: Int) -> Int { return a + b + 100 }
}
이제 구현체가 2개임을 WMO가 확인 했기에 런타임에 외부에서 어떤 구현체를 받을 지 알 수 없게 됨.
그러므로, WMO는 동적 디스패치를 유지
✨근데 WMO는 모듈 내 모든 코드를 알지 않나요?
// 제네릭 T를 사용하여 구체적인 타입 정보를 보존
final class OptimizedMathService<T: Calculator> {
private let calculator: T
init(calculator: T) {
self.calculator = calculator
}
func calculate(x: Int, y: Int) -> Int {
// 컴파일러는 이 제네릭 클래스를 사용하는 코드를 발견하면,
// 해당 타입에 맞춰 별도의 코드를 생성
// 따라서 여기서는 다시 '직접 호출' 및 '인라인화'가 가능
return calculator.add(x, y)
}
}
제네릭을 사용하면 해당 구현체가 생성될 때 타입이 고정된 별도의 코드가 생성
그러므로 WMO는 고정된 타입을 확인하고 제네릭으로 고정된 타입이 들어간 복사된 구현체 코드의 calculate 메서드 내에 고정된 타입의 add 메서드를 하드코딩
Xcode에서 WMO가 어떤 수준으로 적용할 지를 정하는 옵션
WMO 적용 X
최적화 X
디버깅 정보 보존
그에 따른 빌드 속도 ⬆️
Debug 환경에서 사용 추천
공격적인 inline 수행
코드 크기가 커지더라도 실행 속도를 최우선
default 값
Release 환경 추천
inline화 최소화
함수 호출을 유지하여 앱 용량 줄이기
App Clip, WatchOS와 같이 용량이 적을 필요가 있는 환경에 적용
Build Settings -> Swift Compiler -> Code Generation -> Optimization Level
이 함수의 구현체가 컴파일 시점에 다른 모듈 코드에 복사 될 수 있음을 표시하는 매크로
// 프레임워크: CoreLogKit
public protocol Logger {
func log(_ message: String)
}
public struct ConsoleLogger: Logger {
// 내부 속성이지만, @inlinable 함수에서 접근해야 하므로 허용
@usableFromInline
internal let prefix: String
public init(prefix: String = "[Core]") {
self.prefix = prefix
}
/// @inlinable 선언:
/// 이 함수의 구현체는 컴파일 시점에 클라이언트 코드에 복사될 수 있음을 표시
@inlinable
public func log(_ message: String) {
// 내부 메서드 호출 시에도 @usableFromInline이 필요
internalLog("\(prefix) \(message)")
}
@usableFromInline
internal func internalLog(_ formattedMessage: String) {
print(formattedMessage)
}
}
// 앱
import CoreLogKit
final class LogService {
private let logger: Logger
// 의존성 주입 (DI)
init(logger: Logger) {
self.logger = logger
}
func performAction() {
// 컴파일러는 logger가 ConsoleLogger 구체 타입임을 알게 되면,
// log() 함수의 내용을 여기로 가져와서 아래처럼 코드를 바꿈.
// 기존: logger.log("Action Started")
// 최적화 후:
// print("[Core] Action Started")
}
}
@inlineable 매크로를 통해 해당 함수가 모듈 외부에서 WMO등에 의해 최적화가 되어도 좋다라는 걸 명시
@usableFromInline 매크로를 통해 @inlineable 내에서 사용되는 프로퍼티, 메서드들들이 inline 작업 시에 사용될 수 있도록 명시
@usableFromInline 매크로가 사용되는 프로퍼티, 메서드는 public, internal level 이어야 함.
모듈 바깥에서 @usableFromInline 프로퍼티에 read/write 발생 시 컴파일 에러 발생으로 접근자 기능이 올바르게 동작
각 모듈 간 sync: 한 쪽 모듈이 수정된 후 배포되어도 다른 쪽 모듈이 다시 컴파일 되지 않으면 구버전의 복사된 코드 실행
앱 용량 커질 가능성 있음. 모듈 A를 B, C, D, E 등에서 호출하면 모듈 A 코드를 각 모듈에서 복사해서 사용
역공학 위험: 프레임 워크 내부 로직이 클라이언트 측 코드에서 드러나므로, 역공학에 주의
https://phillip5094.tistory.com/103
https://www.swift.org/blog/whole-module-optimizations/
https://developer.apple.com/videos/play/wwdc2019/416/
https://www.swift.org/blog/osize/
https://github.com/swiftlang/swift/blob/main/docs/OptimizationTips.rst