WMO란 Swift의 컴파일러 기술이고 전체 모듈 최적화의 성능 승리는 프로젝트에 크게 의존하지만 최대 2배 또는 5배까지 가능합니다.
전체 모듈 최적화는 -whole-module-optimization(또는 -wmo) 컴파일러 플래그로 활성화할 수 있으며 Xcode 8에서는 새 프로젝트에 대해 기본적으로 켜져 있습니다. 또한 Swift Package Manager는 릴리스 빌드에서 전체 모듈 최적화로 컴파일이 된다고합니다.
모듈은 Swift 파일의 집합이고 각 모듈은 배포단위로 컴파일이 된다 단일 컴파일에서 Swift 컴파일러는 모듈의 각 파일에 대해 개별적으로 호출이된다.
소스 파일을 읽고 구문 분헉을 한후 컴파일러는 Swift 코드를 최적화 , 코드를 생성 그리고 개체 파일을 생성합니다.
그리고 마지막으로 링커가 모든 개체파일을 결합하고 공유 라이브러리 또는 실행파일을 생성한다고 한다.
단일 파일 컴파일에서 컴파일러의 최적화 범위는 단일 파일입니다. 이는 함수 인라인 또는 일반 특수화와 같은 교차 함수 최적화를 동일한 파일에서 호출 및 정의되는 함수로 제한합니다.
예를 들어 보겠습니다. utils.swift라는 이름의 모듈 파일에 메서드가 있는 일반 유틸리티 데이터 구조 Container<T>가 포함되어 getElement있고 이 메서드가 예를 들어 main.swift에서와 같이 모듈 전체에서 호출된다고 가정해 보겠습니다.
getElement 컴파일러가 main.swift 에서 최적화를 할때 구현 방법을 모릅니다(제네릭하게 선언이 되어있기에) , 다만 해당 함수가 존재한다는 것만을 알 고 있다. 따라서 컴파일러는 getElement에 대한 호출을 생성한다 , 그리고 컴파일러가 utils.swwift 를 최적화 할대 함수가 호출되는 구체적인 값이 무엇인지는 알수 없다. 그래서 static이 아니 dynamic dispatch가 이뤄지게 된다.
이렇게 하나 하나를 컴파일을 하게된다면 간단한 제네릭함수조차 상당히 느리게 컴파일이 이뤄지게된다.
하지만 WMO를 통해 컴파일러는 전체적으로 훨씬 더 빠른 작업을 수행을 할 수있고 두 가지의 큰 장점이 있다고한다.
첫째 , 컴파일러는 모듈의 모든 함수 구현을 확인하므로 함수 인라인 및 함수 특수화와 같은 최적화를 수행할 수 있다.
함수 특수화란 컴파일러가 특정 호출 컨텍스트에 최적화된 새 버전의 함수를 생성함을 의미합니다. 예를 들어 컴파일러는 구체적인 유형에 대한 일반 함수를 전문화할 수 있습니다.
Container에서 컴파일러는 구체적인 유형에 특화된 제네릭 버전을 생성한다
그리고 다음 컴파일러는 특수 getElement함수를 add함수에 인라인할 수 있습니다
이 과정은 몇가지 기계 명령어로 컴파일이 된다고 하고 기존의 dynamicfetch 두번 컴파일 한것과 비교했을때 상당히 큰 차이라고한다.
둘째, 컴파일러가 비공개 함수의 모든 사용에 대해 추론할 수 있다는 것입니다. 비공개 함수는 모듈 내에서만 사용할 수 있으므로 컴파일러는 이러한 함수에 대한 모든 참조를 확인할 수 있습니다. 컴파일러는 이 정보로 무엇을 할 수 있습니까?
아주 기본적인 최적화 중 하나는 소위 죽은기능과 메서드를 제거하는 것이라고한다. 이것들은 결코 호출되거나 다른방식으로 사용되지 않는 함수들이다.
전체 모듈 최적화를 통해 컴파일러는 비공개 함수 또는 메소드가 전혀 사용되지 않는지 여부를 알고 제거할수있다.
Container.getElement에서 add함수가 호출되는 유일한 장소 라고 가정해 봅시다. 주석 처리 후 getElement이 함수는 더 이상 사용되지 않으므로 제거할 수 있습니다. 컴파일러가 사용하지 않기로 결정하더라도 함수는 특수 버전만 호출 하기 때문에 getElement컴파일러는 의 원래 일반 버전을 제거할 수 있다
그렇다면 한번에 모든것을 읽기 때문에 더 느려야 하지 않는가?
컴파일 시간
단일 파일 컴파일을 사용하면 컴파일러 드라이버가 병렬로 수행할 수 있는 별도의 프로세스에서 각 파일에 대한 컴파일을 시작합니다. 또한 마지막 컴파일 이후 수정되지 않은 파일은 다시 컴파일할 필요가 없습니다(모든 종속성도 수정되지 않았다고 가정). 증분 컴파일이라고 합니다. 이 모든 것은 특히 약간만 변경하는 경우 컴파일 시간을 많이 절약합니다. 전체 모듈 컴파일에서 어떻게 작동합니까? 컴파일러가 전체 모듈 최적화 모드에서 어떻게 작동하는지 더 자세히 살펴보겠습니다.
내부적으로 컴파일러는 파서, 유형 검사, SIL 최적화, LLVM 백엔드와 같은 여러 단계에서 실행됩니다.
구문 분석 및 유형 검사는 대부분의 경우 매우 빠르며 후속 Swift 릴리스에서는 더 빨라질 것으로 예상합니다. SIL 옵티마이저(SIL은 "Swift Intermediate Language"의 약자)는 일반 특수화, 함수 인라이닝 등과 같은 모든 중요한 Swift 전용 최적화를 수행합니다. 컴파일러의 이 단계는 일반적으로 컴파일 시간의 약 1/3이 걸립니다. 컴파일 시간의 대부분은 저수준 최적화를 실행하고 코드 생성을 수행하는 LLVM 백엔드에 의해 소비됩니다.
SIL 옵티마이저에서 전체 모듈 최적화를 수행한 후 모듈은 다시 여러 부분으로 분할됩니다. LLVM 백엔드는 여러 스레드에서 분할된 부분을 처리합니다. 또한 해당 부품이 이전 빌드 이후 변경되지 않은 경우 해당 부품의 재처리를 방지합니다. 따라서 전체 모듈 최적화를 사용하더라도 컴파일러는 컴파일 작업의 큰 부분을 병렬(다중 스레드) 및 증분으로 수행할 수 있습니다.
결론
전체 모듈 최적화는 모듈의 파일 간에 Swift 코드를 배포하는 방법에 대해 걱정할 필요 없이 최대 성능을 얻을 수 있는 좋은 방법입니다. 위에서 설명한 것과 같은 최적화가 중요한 코드 섹션에서 시작되면 단일 파일 컴파일보다 성능이 최대 5배 향상될 수 있습니다. 그리고 일반적인 모놀리식 전체 프로그램 최적화 접근 방식보다 훨씬 더 나은 컴파일 시간으로 이러한 고성능을 얻을 수 있습니다.
해당 자료글은 https://www.swift.org/blog/whole-module-optimizations/
자료를 번역하여 제가 공부를 해가면서 일정 글을 이해한대로 작성하면서 쓴 글입니다. 제가 이해한대로 적었기때문에 원문의 글을 읽어보시는것을 추천하고 틀린부분이 있다면 언제나 지적은 환영입니다.
'Swift 공부' 카테고리의 다른 글
Swift Throw Error Handling (0) | 2022.04.07 |
---|---|
Async / await의 도입 스위프트 5.5 (0) | 2022.04.05 |
Tuist 모듈화 공부 Swinject 3편 적용기 (0) | 2022.04.03 |
Tuist 모듈화 공부 Swinject 2편 잡담 (0) | 2022.04.01 |
Static Dispatch vs Dynamic Dispatch (0) | 2022.03.30 |