[WWDC22] Link fast: Improve build and launch times (1)

inu
daily-monster
Published in
9 min readApr 1, 2024

안녕하세요 inu입니다. 오늘은 Link fast: Improve build and launch times를 감상하고 정리해봤습니다. 부가적인 정보가 필요한 내용이 많아 추가하며 정리하다보니 내용이 길어져 두편으로 나누어 소개합니다. 오늘은 정적 라이브러리에 대한 내용을 중심적으로 설명드릴게요.

사전지식

오브젝트 파일 : 소스코드 파일이 컴파일러에 의해 컴파일된 이후 생성되는 파일로, 이 파일은 실행가능한 파일(excutable file)이나 라이브러리(library) 파일을 만들기 전 중간 단계의 바이너리 형태입니다.

  • CPU가 바로 실행할 수 있는 저수준의 코드, 전역변수와 정적변수 등의 데이터 섹션, 디버깅 관련정보, 변수 및 함수 이름에 대한 식별자와 그 위치를 매핑한 정보인 심볼테이블 등의 정보가 포함되어 있습니다.
  • 링킹과정에서 여러 오브젝트 파일과 라이브러리가 결합되어 최종적인 오브젝트 파일 혹은 라이브러리 파일이 만들어집니다.
  • 오브젝트 파일을 사용하면 매번 전체 컴파일을 할 필요가 없어지기 때문에 개발과정에서 시간을 절약할 수 있습니다. 변경되지 않은 오브젝트 파일은 재사용하고, 변경된 부분만 다시 컴파일하고 링크할 수 있기 때문입니다.

Linking : 우리들의 코드가 외부 라이브러리 혹은 프레임워크와 결합되어 실행가능한 앱을 생성하는 과정입니다.

  • Static Linking : 앱을 빌드할 때 발생, 빌드시간과 앱의 최종 크기에 영향을 줍니다.
  • Dynamic Linking : 앱이 실행될 때 발생, 앱 실행까지의 대기시간에 영향을 줍니다.

이 중 오늘은 Static Linking에 대해 더 자세히 알아봅시다.

What is static linking?

1970년대, 초기에는 컴파일에 복잡한 처리가 필요없었습니다. 하나의 소스파일(prog.c)에서 컴파일러(cc)를 실행하면 최종적으로 실행가능한 프로그램을 생성해냈습니다. 이는 심플한 과정이지만 이렇게되면 기능이 늘어날수록 소스파일(prog.c)이 방대해진다는 단점이 있었습니다. 그 방대한 소스파일로 인해 사소한 기능 하나하나가 수정될 때에도 모든 코드를 컴파일해야만 했습니다.

그래서 오브젝트 파일(prog.o, other.o)과 정적 링커(ld)가 등장했습니다. 오브젝트 파일은 최종적인 실행파일이 되기 전 중간단계의 바이너리 파일이라고 생각하시면 됩니다. 정적 링커는 소스코드를 컴파일하는 컴파일러(cc)와 별개로 오브젝트 파일을 모아 최종적인 실행가능 프로그램을 만들어내는 역할을 합니다. 이렇게되면 이전과 다르게 각 소스파일의 기능을 잘 구분해놓으면 매번 모든 코드를 컴파일할 필요가 없게됩니다. 수정사항이 없는 영역일 경우 오브젝트 파일을 재사용할 수 있기 때문입니다.

이렇게 변화함에 따라 사람들은 오브젝트 파일을 주고받으며 서로의 프로그램에 좀 더 쉽게 영향을 줄 수 있게 되었습니다. 하지만 오브젝트 파일 여러개를 매번 공유하는 것도 번거롭다고 생각한 누군가가 이들을 하나로 묶으면 좋지 않을까 생각합니다.

이렇게 만들어진 것이 오브젝트 파일의 묶음인 라이브러리입니다. 아카이브 툴(ar)을 사용해 여러개의 오브젝트 파일을 하나의 라이브러리(libc.a)로 묶었습니다. 이를 통해 공통 코드를 공유하는데 크나큰 발전이 생겼습니다. 당시에는 ‘라이브러리’ 혹은 ‘아카이브’라고 불렀지만 이것이 현대에서 말하는 ‘정적 라이브러리(static library)’입니다.

이렇게 서로 많은 공통 코드(라이브러리)를 서로 주고받을 수 있게되면서 프로그램이 불필요하게 방대해지는 문제도 있었습니다. 라이브러리에서 단순히 몇개의 함수만 필요로 하더라도 수천개의 함수를 프로그램에 복사해야했기 때문입니다. 이를 해결하기 위해 라이브러리의 모든 오브젝트 파일을 사용하는 것이 아니라, 현재 내 프로그램에서 필요로하는 오브젝트 파일만 골라서 사용하도록 최적화를 진행했습니다. 현재 프로그램에서 정의되지 않은 심볼을 해결시켜주는 오브젝트 파일을 찾아서 그 파일만 가져오는 것입니다. (*잠시 뒤에 다시 언급하겠지만 이 탐색과정은 추가적인 빌드시간을 필요로 하기 때문에 필요한 파일만 가져오는 최적화가 무조건 좋다고 말하기는 어렵습니다. 앱의 크기를 줄이는 대신 빌드시간을 늘리는 옵션인거죠.)

Recent ld64 improvements

static linking 속도를 평균 2배이상 빠르게 만들었다고 합니다. 여러가지 작업을 병렬적으로 수행하고 알고리즘을 개선했으며, 바이너리의 UUID를 계산할 때 하드웨어 가속을 지원하는 최신 암호화 라이브러리(SHA256)를 사용했다고 하네요. (cf. 하드웨어 가속? 특정 연산을 CPU가 아닌, 그 연산을 특화하여 처리할 수 있는 전용 하드웨어(예: GPU, DSP, TPU 등)를 사용하여 수행하는 기술)

이런 자랑은 사실 제가 엄청 궁금했던 내용은 아닙니다만… 그래도 속도가 빨라졌다니 박수쳐야할 일이겠죠? 애플 파이팅.

Static Linking best practices

static library 내부에 사용되는 일부 소스파일 코드가 수정되면, 라이브러기가 오브젝트 파일로 이루어져 있음에도 불구하고 거의 라이브러리 전체가 재컴파일 되어야할 수 있습니다. 여러 소스파일에 영향을 미치는 일부 파일이 수정되면 그와 연관된 나머지 파일도 모두 재컴파일 되어야하기 때문입니다. 특히 헤더파일처럼 많은 소스파일에 영향을 줄 수 있는 파일이라면 그 영향력은 더 넓을 것입니다. 따라서 코드 변경이 활발히 이루어지는 경우 변경사항을 정적 라이브러리 밖으로 이동하는 것을 고려해보면 좋습니다.

앞서 static library의 선택적 로딩을 통한 최적화 과정에 대해 살펴봤습니다. 선택적 로딩은 앱의 실행파일의 용량을 줄여주지만 링킹과정이 느려진다는 단점이 있었습니다. (이는 빌드가 항상 같은 결과를 만들어내도록 하기 위해 오브젝트 파일을 항상 고정적이고 직렬적인 순서에 따라 처리하기 때문입니다. 병렬화의 이점을 활용하지 못하는 것입니다.)

-all_load

링커에게 정적 라이브러리에 포함된 모든 오브젝트 파일을 앱에 로드하라고 지시하는 옵션입니다. 선택적 로딩을 사용하더라도 결국 라이브러리 대부분의 오브젝트 파일을 로드하는 경우라면 유용할 수 있습니다. 이 경우 오브젝트 파일을 순차적으로 탐색할 필요가 없어지기 때문에 병렬화의 이점을 활용할 수 있습니다. 다만 여러개의 라이브러리에서 동일한 심볼을 사용하는 경우 별도의 처리를 해주지 않으면 문제가 발생할 수 있습니다. 또한 불필요한 코드까지 전부 앱에 포함되어 최종 바이너리의 크기가 커질 수 있다는 단점이 있습니다.

-dead_strip

이 옵션을 사용하면 링커가 사용되지 않는 코드(접근되지 않는 함수 등)를 최종 바이너리에서 제거합니다. 앱의 크기를 줄이고 성능을 향상시키는 데 유용합니다. -dead_strip은 앞서 설명한 -all_load와 함께 사용할 때 특히 유용합니다. -all_load로 인해 불필요하게 많은 코드가 포함될 수 있으나, -dead_strip 옵션을 추가하면 사용되지 않는 코드를 제거하여 바이너리 크기의 증가를 최소화할 수 있는 것입니다.

결과적으로 개발자는 기본 옵션인 선택적 로딩을 사용했을때와 -all_load, -dead_strip 옵션으로 처리했을때의 링커 시간을 측정해 어떤 선택지가 더 유리한지 확인할 필요가 있습니다. Xcode의 Build Settings에서 해당 옵션을 처리할 수 있습니다.

-no_exported_symbols

이 옵션을 사용하면 링커가 실행 파일에서 내보내기(export) 심볼들을 제외하도록 지시합니다. 만약 현재 처리하려는 링크가 메인 실행 바이너리일 경우 이 옵션을 통해 내보내기 심볼을 제외하여 속도를 빠르게 만들 수 있습니다. 내보내기 심볼은 주로 다이나믹 라이브러리에서 필요로 하는 옵션으로, 외부에서 내부 심볼에 접근할 수 있는 인터페이스 역할을 합니다. 메인 실행 바이너리는 외부에서 접근할 가능성이 적기 때문에 해당 옵션을 적용해 내보내기 심볼을 아예 만들지 않도록 할 수 있습니다. (단, 앱이 메인 실행 파일로 연결되는 플러그인을 로드하거나 XCTest 처리를 위해 메인 실행 파일을 이용하는 등의 특수케이스에서는 내보내기 심볼이 필요할 수 있습니다.)

dyld_info -exports /path/to/binary | wc -l

위 명령어를 통해 내보낸 심볼의 개수를 계산할 수 있습니다. 특정 앱에서 이 명령어를 통해 내보낸 심볼의 개수가 100만개 정도되는 것을 확인했습니다. 이 앱의 경우 -no_exported_symbols 옵션을 통해 앱의 링크시간이 2~3초 정도 단축되었습니다. (dyld_info 옵션에 대해서는 잠시 뒤에서 더 자세히 알아봅시다.)

-no_deduplicate

링크 과정에서 중복된 코드나 데이터의 제거(중복 제거, deduplication)를 하지않도록 만드는 옵션입니다. 일반적으로 중복제거는 최종 실행 파일을 상당히 줄일 수 있어 유용하게 사용됩니다. 하지만 중복제거는 고비용 알고리즘을 필요로 하기 때문에 빌드시간에 악영향을 줍니다. 따라서 최종 실행 파일보다는 빌드 속도가 더 중요한 디버깅환경에서는 해당 옵션을 사용하는 것이 도움이 됩니다.

이 역시 엑스코드 내부에서 옵션을 설정할 수 있습니다.

--

--