Whole-Module Optimization in Swift 3
팀내에서 빌드 속도와 관련되어 여러가지 연구를 하고 있는데요.
그중에 기본이 되는 컴파일러 이해와 관련해서 wmo에 대해서 swift.org에 올라온 글을 번역 및 요약해 보았습니다.
해당 글을 https://swift.org/blog/whole-module-optimizations/ 에서 확인 하실수 있습니다.
바쁘신 분들은 요약만 읽어보셔도 좋을것 같습니다.
일부 의역이 있음을 알려드립니다. 번역 오류 및 추가의견은 댓글로 남겨주시면 감사하겠습니다.
단일파일 컴파일의 경우, 모듈내 파일들을 하나씩 컴파일하게됨
한계: 파일내에 컴파일만 이루어짐
1) function inlining 이 안됨
2) function specialization 이 안됨
결국 위의 한계때문에 컴파일 시간 길어짐
장점1: 단일 파일 컴파일에서 한계를 극복(모듈 전체를 확인함)
function inlining 가능
function specialization 가능
이외에도 추가적인 도움이 많이 있음. (불필요한 referencing counting 제거, 적은수의 기계명령어 생성 등..)
장점2: non-public 함수에 대한 분석
죽은 함수 찾아내기
컴파일 최적화 > 컴파일 시간 단축
컴파일 과정 : 파싱, 타입체킹, SIL , LLVM 백엔드
파싱 및 타입체킹: 엄청 빨리 끝나는 작업
SIL : 전체 컴파일 시간의 1/3 정도 차지
LLVM: 나머지 컴파일 시간을 차지(2/3 정도 차지)
SIL 작업에서 wmo 가 최적화 하고, 다시 LLVM 백엔드 작업시 병렬로 처리가 되기 때문에 문제가 없다.
wmo 는 스위프트 코드를 모듈내에 어떻게 분산시켜야 하는지에 대한 걱정없이 최고의 효율성을 가질수 있는 방법입니다. 만약 최적화가 매우 중요한 코드 섹션에 들어가게 되면, 성능은 단일 파일 컴파일 할때보다 최대 5배 가량 향상될수 있습니다. 그리고 이방법은 일반적인 “프로그램 전체 최적화” 방법 보다 더 짧은 컴파일 시간을 가질수 있게 됩니다
wmo 는 스위프트 컴파일러의 최적화 모드이다.
퍼포먼스는 사실 프로젝트에 따라 매우 다르지만, 그것이 성능차이가 2~5배 날수가 있다.
wmo는 -whole-module-optimization(or -wmo) 컴파일러 플래그를 이용하여 킬수 있다.
기본적으로 xcode 8 에서 새로 만든 프로젝트에는 디폴트로 켜저 있다.
그래서 이게 다 뭐에 관한거냐?, 그전에 먼저 wmo 업을때의 컴파일러 동작방식을 보자
일단, 모듈은 스위프트 파일들의 묶음이다. 각 모듈은 하나의 유닛 단위로 컴파일된다.
단일 파일 컴파일에서는 모듈의 각 파일에 대해 스위프트 컴파일러가 호출됩니다. 사실 이게 뒤에서 일어나는 일입니다. 사용자로서 당신이 수동으로 할필요 없습니다. 이것은 엑스코드 빌드 시스템 혹은 컴파일러 드라이버에 의해 자동으로 되는 것이긴 합니다.

소스파일을 읽고 파싱하고나서, 컴파일러는 스위프트 코드를 최적화하고, 기계코드 만들고, 오브젝트 파일을 만듭니다
마지막으로, 링커가 모든 오브젝트 파일들을 합치고, 공유 라이브러리 혹은 실행가능한 것을 만듭니다.
단일 파일 컴파일에서는 컴파일러의 최적화 영역은 오직 단일 파일 하나입니다. 이것은 function-inlining 혹은 generic specialization과 같은 cross-function 최적화에 있어서 한계가 있습니다.
한번 예를 봅시다. 어떤 모듈안의 utils.swift 란 파일에 generic 유틸용 자료구조 Container 가 있다고 가정해봅시다. Container 는 getElement 란 메소드도 가지고 있으며, 해당 모듈안에서 곳곳에서 호출됩니다. 예를 들어 main.swift에서 호출된다고 합시다.
컴파일러가 main.swift를 최적화할때, getElement 가 어떻게 구현되어있는지는 모릅니다. 그냥 그게 쓰인다는것만 알고 있죠. 그래서 컴파일러가 getElement 호출을 합니다. 반대로, 컴파일러가 utils.swift 를 최적화 할때, 어떤 유형의 함수가 호출 되는지 알수가 없습니다. 그래서 컴파일러는 구체적인 형태의 함수보다 느린 generic 형태의 함수를 생성할수 밖에 없게 됩니다.
심지어 getElement에서 단순히 리턴만 하는 부분도 어떻게 인자를 복사하는지 알기 위해, 타입의 메타데이터를 검색하게 됩니다. 그 타입이 단순히 Int 타입이 될수 도 있지만, 더큰 무언가가 될수도 있고, 그것은 어쩌면 referencing couting을 수반하는 작업일수도 있습니다. 컴파일러는 그냥 모릅니다.
wmo 와 함께라면 컴파일러는 훨씬더 잘할수 있습니다. -wmo 옵션과 함께 컴파일링 하면, 컴파일러는 모듈안에 있는 스위프트 파일 전체를 통으로 최적화 시키게 됩니다.
이것은 크게 2가지 장점이 있습니다.
첫번째는, 컴파일러가 모듈안에 함수가 구현된상태를 모두 확인합니다. 이렇게 하기때문에 function-inlining 혹은 function 구체화 같은 최적화를 수행할수 있게 됩니다. 함수 구체화는 다음과 같습니다. 컴파일러가 최적화를위해서 특정 호출 상황에 맞는 함수를 새로 만들수 있습니다.
In our example, the compiler produces a version of the generic Container which is specialized for the concrete type Int.
Then the compiler can inline the specialized getElement function into the add function.
이것은 적은수의 기계명령어로 컴파일 시켜줍니다. 사실 이것은 파일 하나씩 할때와는 매우 큰차이를 보여줍니다.
함수구체화 혹은 파일간의 inlining 들은 단순히 컴파일러 최적화의 한예이다. 컴파일러가 함수를 inlining안해도, 함수 구현을 볼수 있는 것만으로 많은 도움이 된다. 예를 들어 reference counting 관련하여 분석을 하는데도 도움이 됩니다. 따라서 컴파일러는 함수 호출과 관련된 불필요한 reference counting 작업을 제거할수 있습니다.
두번째 wmo 의 장점은 non-public 함수 사용에 대한 분석을 할수 있다는 점입니다. non-public 함수는 모듈안에서만 사용하게 됩니다. 그래서 컴파일러는 그러한 non-public 함수에 대한 모든 참조를 확실히 볼수 있게 됩니다. 그럼 컴파일러는 이러한 정보를 가지고 뭘 할수 있을까요?
한가지 기본적인 최적화 기법은 소위 말하는 죽은 함수 없애기 입니다. 죽은 함수라 하면,, 코드내에서 이제 절대 안불리는 녀석들을 의미 하게 됩니다. wmo를 킨 상태에서 컴파일러는 어떤 함수가 한번도 안쓰인다고 판단되는 녀석을 발견하면 그런경우에는 제거를 하게 됩니다. 그렇다면 왜 프로그래머들이 한번도 안쓰이는 함수를 작성할까요? 음 이건 사실 죽은 함수 제거를 위한 가장 중요한 케이스는 아닙니다. 종종 함수들은 다른 최적화의 부작용으로 죽게 되기도 합니다.
한번, Container.getElement 이 호출되는 곳에서만 add 함수가 위치한다고 생각해 봅시다. Container.getElement를 inlining 하고 나면, 이 함수는 이제 안쓰이니까 지울수 있습니다. 비록 컴파일러가 getElement 를 inlining을 하지 않아도, 컴파일러는 원래 generic 형태의 getElement 는 지울수가 있습니다. 왜냐하면 add 함수가 특정 버젼(add가 쓰일수 있는 형태)에만 불리기 때문이죠.
단일 컴파일을 사용하면, 컴파일러 드라이버는 각파일에 대한 컴파일을 개별 프로세스(각 프로세스는 병렬로 수행될수 있음)에서 시작하게 됩니다. 또한 이미 컴파일 했던 파일들을 다시 재컴파일 될필요가 없습니다(모든 의존성도 수정이 없다고 가정했을때). 그것을 incremental compilation이라고 말합니다. 이것들을 컴파일 시간을 많이 절약해줍니다. 특히 작은 수정만 한경우가 해당이 됩니다. 그렇다면 이것은 wmo 에서는 어떻게 동작하나요? 한번 컴파일러가 wmo 옵션에서 어떻게 동작하는지 자세히 들여다 봅시다.
내부적으로, 컴파일러는 파서, 타입 체킹, SIL 최적화, LLVM 백엔드와 같은 여러 단계로 실행이 됩니다.
파싱과 타입체킹은 대부분 엄청 빠르며, 이후의 스위프트 버젼에 대해서는 점점 빨라질것으로 보입니다.
SIL 최적화는 (SIL stands for “Swift Intermediate Language”) generic specialization, function inlining과 같은 스위프트 관련된 최적화를 실행합니다. 주로 SIL 최적화는 컴파일 시간의 1/3 정도시간을 소요합니다. 대부분의 컴파일 시간은 lower-level 최적화 및 코등 생성등의 작업을 하는 LLVM 백엔드에서 소요가 됩니다.
wmo 에서 SIL 최적화가 수행되고 나서 모듈을 다시 여러개의 파트로 분리가 됩니다. LLVM 백엔드에서는 앞서 분리된 여러개의 파트들을 멀티 스레드에서 처리합니다. 이 과정에서도 이전에 빌드되고 수정이 안된 부분은 재처리가 안되도록 합니다. 그래서 wmo 가 켜져있어도, 컴파일러는 많은부분의 컴파일 작업을 병렬 처리 및 incrementally 수행을 할수가 있습니다.
wmo 는 스위프트 코드를 모듈내에 어떻게 분산시켜야 하는지에 대한 걱정없이 최고의 효율성을 가질수 있는 방법입니다. 만약 최적화가 매우 중요한 코드 섹션에 들어가게 되면, 성능은 단일 파일 컴파일 할때보다 최대 5배 가량 향상될수 있습니다. 그리고 이방법은 일반적인 “프로그램 전체 최적화” 방법 보다 더 짧은 컴파일 시간을 가질수 있게 됩니다.