빠르게 링크: 빌드 및 실행 시간 개선 (Link fast: Improve build and launch times) - WWDC22
목차
- What is static linking?
- 몇 가지 예와 정적 링킹이 무엇인지 정의
- Recent ld64 improvements
- 애플의 정적 링커인 ld64의 신기능
- Sattic linking best practices
- 정적 링킹에 대한 배경시직과 함께 모범 사례에 대해서 설명
- What is dynamic linking?
- 동적 링킹 과정에서 어떤 일이 발생하는지 다룸
- Recent dyld improvements
- 올해 dyld의 새로운 기능
- Dynamic linking best practices
- 동적 링크 시간 성능 개선을 위해 할 수 있는것
- New tools
- 바이너리에 무엇이 있고, 동적 링킹 과정에서 어떤 일이 일어나는지 확인


라이브러리나 프레임워크의 형태로 우리 코드가 다른 라이브러리의 코드를 사용하려면 링커가 필요함.
링킹에는 두가지 유형이 존재
- 정적 링킹 (static linking)
- 앱을 빌드할 때 발생
- 앱을 빌드하는데 걸리는 시간과 앱의 최종 크기에 영향
- 동적 링킹 (dynamic linking)
- 앱이 실행될 때 발생
- 앱이 작동할 때까지 사용자의 대기 시간에 영향을 줄 수 있음.

초기에는 프로그램들이 간단했고, 소스 파일도 하나 뿐이라서 단 하나의 소스 파일 에서 컴파일러를 실행하면 실행 가능한 프로그램을 생성하지만 하나의 소스파일만 가지고는 프로그램을 확장하기가 쉽지가 않음.
여러 파일을 컴파일 하기 위해서 처음에는 컴파일러를 여러개로 나누는 방향으로 접근.
오브젝트 파일을 읽고 실행 가능한 파일로 연결하는 링커를 정적 링커 `ld`라고 부름.


소프트웨어가 더 많이 발전하면서 사람들은 .o 파일들을 주고 받게 됨.
.o 파일이 늘어마녀서 사람들이 묶어서 제공하자고 함.
처음에는 파일 묶는 아카이브 도구 ('ar')을 사용 (백업 및 배포에 사용)

따라서 워크플로우가 위와 같이 바뀜.
여러개의 .o 파일을 하나의 아카이브로 'ar'화 하고 아카이브 파일에서 직접적으로 .o 파일들을 읽도록 링커가 향상됨.
당시에는 라이브러리 또는 아카이브라고 불렀음. (오늘날에는 정적 라이브러리라고 부름)
하지만 라이브러리의 수천개의 함수가 프로그램에 복사되었기 때문에 최종 프로그램이 무거워짐.
최적화가 필요했고, 링커가 정적 라이브러리의 모든 .o 파일을 사용하는 대신 정의되지 않은 심볼 문제가 해결되는 경우의 .o 파일만 정적 라이브러리에서 가져옴.
모든 C 표준 라이브러리 함수를 포함하는 거대한 하나의 lib.a 정적 라이브리러를 구축할 수 있음을 의미.
모든 프로그램이 하나의 libc.a에 연결할 수 있지만, 각 프로그램은 실제로 필요로하는 libc의 일부만 가져옴.
오늘날에도 이 모델은 존재.

하지만 정적 라이브러리의 선택적 로딩은 분명하지 않고 개발에 있어서 어려움에 빠지기도 함.
정적 라이브러리의 선택적 로딩을 좀 더 명확히 하기 위한 간단한 시나리오를 알아보고자 함.
각각을 .o 파일을 컴파일하면, 위의 이미지처럼 생성되고 회색 박스가 아닌 부분은 정의되지 않은 함수이기 때문이며 심볼의 사용이지 정의를 의미하는 것이 아님.



bar.o와 baz.o를 정적 라이브러리 하나로 합치며, 두개의 .o파일과 이 정적 라이브러리를 링크.
실제로 일어나는 일을 순서대로 알아보자.
- 1. 링커는 명령어 라인 순서대로 파일을 읽어나감.
- 가장 먼저 찾은건 main.o이고, main.o를 로드하고 심볼 테이블에 보이는 main대한 정의를 찾음.
- 하지만 main 안에 정의되지 않은 foo가 있다는 것도 찾음.
- 2. 그 다음 링커는 명령어 라인의 다음 파일 foo.o를 파싱.
- 이 파일은 foo에 대한 정의를 추가하며 즉, 더 이상 정의되지 않은 foo가 아닌 것.
- 하지만 foo.o의 로드는 정의되지 않은 새 심볼 bar또한 추가
- 명령어 라인의 모든 .o파일이 로드되어서 링커는 정의되지 않은 심볼이 남아 있는지 확인
- 이 경우 아직 'bar'가 정의되지 않았기 때문에 링커는 명령어 라인의 라이브러리들을 살펴보면서 정의되지 않은 'bar' 심볼을 충족하는 라이브러리를 찾음
- 링커가 정적 라이브러리 bar.o에서 'bar' 심볼이 정의되는 것을 찾음
- 따라서 링커는 아카이브(libbarbaz.a)로 부터 bar.o를 로드
- 이 시점에서는 더 이상 정의되지 않은 심볼이 없어서 따라서 라이브러리에서 찾기를 중단


- 3. 링커는 다음 단계로 이동해 프로그램에 포함 될 모든 함수와 데이터에 주소를 할당
- 모든 함수와 데이터를 출력 파일에 복사하여 출력 프로그램을 완성
- baz.o는 정적 라이브러리에 있지만, 프로그램에는 로드되지 않음.
- 링커가 정적 라이브러리에서 선택적으로 로드한 방식 때문에 로드되지 않음.
- 이 방식은 분명하진 않아도 정적 라이브러리의 핵심적인 측면.
여기까지가 정적 링킹 및 정적 라이브러리의 기본과 이해.

ld64 최적화에 많은 시간을 들여서 링커는 많은 프로젝트에서 2배 더 빠름.
링커 작업을 병렬로 수행하도록 여러개의 코어를 사용할 수 있는 여러 영역을 찾음

- 입력 파일의 내용을 출력 파일로 복사
- lINKEDIT의 부분을 병렬 구축
- UUID 계산을 변경하고 코드 서명 해시를 병렬로 수행하는 것도 포함
- 여러 알고리즘을 개선
- 각 심볼의 문자열 슬라이스를 나타내는데 C++의 string_view 객체를 사용하게 되면 exports-trie builder가 굉장히 잘 작동하는 것을 볼 수 있음.
- 바이너리의 UUID를 계산할 때 최신 암호화 라이브러리를 사용해 하드웨어 가속을 활용
- 다른 알고리즘 개선도 존재

링커의 성능 개선 작업 동안 일부 앱에서 링크 시간에 영향을 미리는 구성 문제를 발견.
우리 프로젝트에서 링크 시간 개선을 위해 할 수 있는 다섯 가지 주제를 다룸.

- 1. 정적 라이브러리의 사용 여부
- 2. 링크 시간에 큰 영향을 주는 세가지 옵션 (잘 알려지지 않음)
- `-all_load` or `-force_load`
- `-no_exported_symbols`
- `-no_deduplicate`
- 3. 정적 링크의 속성

- 1. 정적 라이브러리에 빌드되는 소스 파일에 열심히 작업해서 빌드 시간이 느려지는 경우
- 파일이 컴파일 되고 나면 목차를 포함한 정적 라이브러리 전체를 다시 빌드해야 하기 때문에 이런 현상이 발생
- 단지 추가적인 I/O가 많아지는 것
- 안정적인 코드에는 정적 라이브러리그 가장 적합
- 코드가 활발하게 변경되지 않는 경우
- 빌드 시간을 줄이기 위해서는 현재 개발 중인 코드를 정적 라이브러리 밖으로 이동하는 것을 고려
- 파일이 컴파일 되고 나면 목차를 포함한 정적 라이브러리 전체를 다시 빌드해야 하기 때문에 이런 현상이 발생
- 2. 아카이브에서 선택적으로 로딩하는 것을 확인했는데, 선택적 로딩의 단점은 링커가 느려진다는 것
- 왜냐하면 빌드를 재현 가능하도록 하면서 전통적인 정적 라이브러리의 의미를 따르기 위해서는 링커가 라이브러리를 고정된 직렬 순서로 처리해야 하기 때문
- 즉, ld64의 병렬화 이점을 정적 라이브러리와 함께 사용할 수 없다는 것
- 이런 기록하는 동작이 별로 필요하지 않은 경우라면 링커 옵션을 사용해 빌드 속도를 높일 수 있음.
- 왜냐하면 빌드를 재현 가능하도록 하면서 전통적인 정적 라이브러리의 의미를 따르기 위해서는 링커가 라이브러리를 고정된 직렬 순서로 처리해야 하기 때문

- 링커 옵션 `-all_load`
- 모든 정적 라이브러리의 모든 .o 파일을 로드하도록 링커에 지시
- 이는 앱이 선택적 로딩을 하지만 결국 모든 정적 라이브러로부터 대부분의 콘텐츠를 로드하는 경우라면 유용
- `-all_load`의 사용은 링커가 모든 정적 라이브러리와 해당 콘텐츠를 병렬로 파싱할 수 있도록 함.
- 동일한 심볼을 구현하는 여러 정적 라이브러리가 있어서 명령어 라인의 정적 라이브러리 순서에 의존해 어떤 구현을 사용할지 정하는 방식이라면 이 옵션은 적합하지 않음.
- 링커가 모든 구현을 로드하기 때문에 일반 정적 링킹 모드에서 찾을 수 있는 심볼의 의미를 가져오리란 법이 없기 때문.
- `-all_load`의 또 다른 단점은 프로그램이 무거워 질 수 있다는 것 `unused` 코드가 계속 추가되기 때문.
- 이를 보완하기 위해서 `-dead_strip` 링커 옵션을 사용할 수 있음
- 이 옵션으로 연결 불가한 코드와 데이터를 링커가 제거
- `-dead_strip` 알고리즘은 빠르게 출력 파일의 사이즈를 줄임으로써 자체적으로 크기를 절약
- `all_load`, `dead_strip`를 함께 사용한다면 이들 옵션의 유무에 따른 링커 시간을 측정해서 우리 케이스에 유리한 선택인지 확인해야 함
- 이를 보완하기 위해서 `-dead_strip` 링커 옵션을 사용할 수 있음

- 링커 옵션 `-no_exported_symbols`
- 링커가 생성하는 LINKEDIT 세그먼트의 한 부분은 하나의 exported-trie로 preix trie(접두사 트리)이며 내보낸 모든 심볼 이름, 주소 및 플래그를 인코딩 함.
- 모든 dylib에는 내보낸 심볼이 있어야 하는 반면 메인 앱 바이너리는 일반적으로 내보낸 심볼이 필요하지 않음.
- 즉, 일반적으로 메인 실행 파일에서는 심볼을 조회하지 않음.
- 이런 케이스에 `-no_exported_symbols`를 사용하여 앱 타겟에 대해 LINKEDIT에서 trie 데이터 구조 생성을 건너뛰도록 할 수 있음.
- 따라서 링크 시간이 개선됨.
- 메인 앱 실행 파일로 다시 연결되는 플러그인을 로드하거나 xctest 번들 실행을 위해 호스트 환경으로 앱과 xctest를 사용하면 앱에 exports가 있어야 함.
- 즉, 해당 구성에서는 `-no_exported_symbols`를 사용할 수 없다는 것
- 사이즈가 큰 경우엔 exports trie의 억제가 합리적임.
- dyld_info 명령을 실행하여 내보낸 심볼의 수를 계산할 수 있음
- 해당 옵션을 통해 링커가 해당 심볼 수에 대한 exports trie를 빌드하는데 시간을 단축할 수 있음.

링커 옵션 `-no_deduplicate`
- 명령은 같지만 이름이 다른 함수를 병합하기 위해서 새로운 패스를 추가
- C++의 템플릿 확장으로 그런 함수들을 많이 얻음.
- 알고리즘은 비싸며 중복을 찾기 위해서 링커는 모든 함수의 명령을 반복적으로 해시해야 하기 때문
- 비용 문제 때문에 링커가 weak-def된 심볼만 살펴보도록 알고리즘을 제한함
- 템플릿 확장으로 C++ 컴파일러가 내보내는 인라인 되지 않은 것들을 제한.
- de-dup 중복 제거는 사이즈 최적화로 디버그 빌드는 사이즈가 아닌 속도 빠른 빌드에 관함.
- Xcode는 디버그 구성을 위해 링커에 `-no_deduplicate`를 전달해 de-dup 최적화를 비활성화 함
- clang 링크 라인을 `-O0`으로 실행할 경우 no-dedup 옵션을 링커에 전달.
- 요약하자면 C++를 사용하고 custom build system를 가진 경우 링크 시간 개선을 위해서 디버그 빌드가 `no_dedeuplcate`를 추가하도록 해야함.
- Xcode에서 비표준 구성을 사용하거나 다른 빌드 시스템을 사용하는 경우

Xcode Build Settings > Other Linker Flags에서 변경할 수 있음.
`all_load`, `-Wl`, `-no_exported_symbols`, `no_deduplicate` 등이 존재

- 정적 라이브러리에 소스 코드가 빌드되고 앱이 이 라이브러리에 연결되지만 해당 코드는 최종 앱에 포함되지 않음.
- 예를 들어 어떤 함수에 `__attribute__((used))`를 추가했다거나 Objective-C 카테고리가 있는 경우 링커가 선택적 로딩을 수행하기 때문에 정적 라이브러리에 있는 .o 파일이 링크 과정 중에 필요한 일부 심볼 또한 정의하지 않는다면 해당 .o 파일은 링커가 로드하지 않게 됨.
- 정적 라이브러리와 `Dead stripping`간의 상호작용도 흥미로운데, 정적 라이브러리의 문제를 숨길 수 있음.
- 일반적으로 심볼의 누락이나 중복되는 심볼은 링커의 오류를 유발
- 하지만 `Dead stripping`은 링커가 메인에서 시작해서 모든 코드와 데이터에 걸쳐 도달 가능한 패스를 실행하도록 해서 심볼의 누락이 연결할 수 없는 코드에서 온 것으로 판명되면 링커가 심볼 누락 오류를 억제하도록 만듦.
- 유사하게 정적 라이브러리에 중복되는 심볼이 있는 경우에 링커는 첫번째 것을 선택하고 오류를 내뱉지 않음
- 정적 라이브러리를 사용할 때 하나의 정적 라이브러리가 여러 프레임워크에 통합되는 경우 각각의 프레임워크는 개별적으로는 잘 실행되지만 어느 시점에서 일부 앱은 두 가지 프레임워크를 모두 사용하게 되고 이는 런타임에 문제를 발생시킴.
- 가장 흔한 경우는 Objective-C 런타임 경고이며 동일한 클래스 명의 여러 인스턴스에 대한 경고.
- 종합적으로 정적 라이브러리는 강력하며 이러한 위험을 피하기 위해서는 정확한 이해가 필요.

정적 라이브러리와 정적 링킹에 대한 다이어그램 먼저 살펴보자.

시간이 지나면서 이 다이어그램이 확장될 것을 생각해보면 점점 더 많은 소스코드가 생김.
사용할 수 있는 라이브러리가 점점 많아질수록 최종 프로그램의 크기가 커짐.
시간에 따라 해당 프로그램 빌드에 들어가는 정적 링크 시간도 증가.


'ar'에서 'ld'로 바꾼다면 출력 라이브러리는 이제 실행 가능한 바이너리가 됨.
'ar'에서 'ld'로 바꾸는 것이 90년대 동적 라이브러리의 시작.
약칭으로 동적 라이브러리를 'dylibs'라고 함.
다른 플랫폼에서는 'DSO'(Dynamic Shared Objects), 'DLL(Dynamic-Link Library)'이라고 부름.

핵심은 정적 링커가 동적 라이브러리와의 링킹을 다르게 취급한다는 점.
라이브러리에서 최종 프로그램으로 코드를 복사하는 것 대신 링커가 일종의 Promise를 기록함.
동적 라이브러리에서 사용되는 심볼 및 런타임 시 라이브러리의 경로를 기록하는 것.
이것의 장점으로는 프로그램 파일 사이즈가 개발자의 통제 하에 있을 수 있게 됨.
즉, 우리가 작성한 코드와 런타임에 필요한 동적 라이브러리 목록만 포함됨.
라이브러리 코드의 복사본을 더 이상 프로그램에 받지 않게 됨.
프로그램의 정적 링크 시간은 이제 코드 크기에 비례하고 링크하는 dylib 수와는 무관함.

이로 인해서 또한 가상 메모리 시스템을 활용할 수 있음.
여러 프로세스에서 동일한 동적 라이브러리가 사용되는 경우 가상 시스템은 그 dylib를 사용하는 모든 프로세스에서 해당 dylib에 대해 동일한 물리적 RAM 페이지를 재사용함.
여기까지가 동적 라이브러리의 시초와 이를 통해 해결한 문제를 같이 확인함.

동적 라이브러리의 이점
- 동적 라이브러리 사용의 이점은 빌드 시간 단축
동적 라이브러리의 비용
- 1. 앱 시작 속도의 둔화
- 앱 시작시 프로그램 파일 하나만 로드하는 것이 아니기 때문
- 모든 dylib들이 로드되고 함께 연결되어야 함.
- 빌드 시간에서 시작 시간으로 링킹 비용의 일부를 전가.
- 2. 동적 라이브러리 기반의 프로그램은 더티 페이지가 더 많음.
- 정적 라이브러리의 경우 모든 정적 라이브러리의 모든 부분들을 링커가 메인 실행 파일의 동일한 DATA 페이지에 다 같이 배치
- 하지만 dylib를 사용하면 DATA페이지가 각 라이브러리에 있음.
- 3. dyld라는 동적 링커가 필요함.
- 빌드 시에 실행 파일에 기록된다던 Promise 부분에서 라이브러리를 로드하기 위해 Promise를 처리할 무언가가 런타임에 필요하기 때문


이것이 바로 dyld 동적 링커의 용도임.
동적 링킹이 런타임에서의 동작
- 1. 실행 가능한 바이너리는 세그먼트로 분할
- 일반적으로 최소한 TEXT, DATA, LINKEDIT 으로 분할.
- 세그먼트는 항상 OS 사이즈의 배수로 각 세그먼트는 다른 권한을 가짐
- 예를 들어 TEXT는 '실행' 권한을 가짐
- CPU가 페이지의 바이트를 기계 명령어로 처리 가능함을 의미.
- 2. 런타임 시 dyld는 여기 보이는 것처럼 각 세그먼트의 권한을 받아 실행 파일들을 메모리에 mmap() 해야 함.
- 세그먼트들은 페이지 사이즈이고 페이지 정렬되기 때문에 가상 메모리 시스템이 프로그램이나 dylib 파일을 VM 범위의 백업 저장소로 설정하는 것이 간단함.
- 즉, 해당 페이지에 메모리 접근이 있을때 까지 RAM에 아무것도 로드되지 않으며 이로 인해 페이지 폴트가 발생하고 이는 VM 시스템이 파일의 적절한 하위 범위를 읽고 그 콘텐츠로 필요한 RAM 페이지를 채우도록 만듦.
- 하지만 매핑만으로는 충분하지 않고 어떻게든 프로그램은 연결(link)되거나 dylib에 바인딩되어야 하는데, 이를 위해서 'fix ups'이란 개념이 존재함.

TEXT, DATA, LINKEDIT으로 구성된 mach-o 파일을 보자
TEXT는 불변이며, 코드 서명을 기반으로 하는 시스템 안에 있어야 함.
malloc의 상대주소는 프로그램이 빌드될 때 알 수 없고, 일어나는 일은 malloc이 dylib에 있는 것을 보고 정적 링커가 호출 사이트를 변환
이때 동일한 TEXT 세그먼트의 링커에 의해서 호출 사이트는 스텁에 대한 호출이 됨.
따라서 빌드시에 상대 주소가 알려지고 이는 bl 명령어가 올바르게 형성될 수 있음을 의미
이로인해서 스텁이 DATA로 부터 포인터를 로드하고 해당 위치로 점프함.
즉, 런타임에서는 TEXT에 대한 변경이 필요하지 않음.
dyld에 의해 DATA만 변경됨.
dyld를 이해하는데 중요한 것은 dyld에 의한 모든 수정이 단지 DATA에 대한 dyld의 포인터 설정 뿐이라는 것.


dyld가 수행하는 수정사항에 대해 더 자세히 보자면 LINKEDIT 어딘가에 수정 작업에 써야하는 정보가 있음.
'fixups'에는 두가지 유형이 존재.
- 첫번째 유형은 Rebases
- dylib 또는 app 자체 내에서 가리키는 포인터를 갖는 경우.
- ASLR이라는 보안기능이 존재하는데 dyld가 임의의 주소에서 dylib를 로드하도록 만듦.
- 따라서 내부 포인터는 빌드 시에 설정할 수 없으며, 이런 포인터들은 시작 시에 dyld가 조정하거나 'Rebases'를 해야 함.
- dylib가 주소 0에서 로드되는 경우에 디스크에서 이런 포인터들은 target address들을 포함함.
- 따라서 LINKEDIT이 기록해야 하는건 각 'Rebases'의 위치가 전부임.
- dyld는 dylib의 실제 로드 주소를 각 'Rebases' 위치에 추가해 올바르게 수정할 수 있음.
- 두번째 유형은 Binds
- 'Binds'는 symbolic reference로 target이 숫자가 아닌 symbol 이름.
- 'malloc' 함수에 대한 포인터를 예로 하자면, 문자열 '_malloc'는 실제로 LINKEDIT에 저장되며, dyld는 해당 문자열을 사용해 libSystem.dylib의 exports trie에서 mallo의 실제 주소를 조회
- 그 다음 dyld는 해당 값을 'Binds'가 지정한 위치에 저장

WWDC22에서는 fixups을 인코딩하는 새로운 방법으로 'chained fixups'을 새롭게 발표.
- 'chained fixups'의 첫번째 장점은 LINKEDIT이 더 작아짐.
- 모든 수정 위치를 저장하는 대신 새 형식은 각 DATA 페이지의 첫번째 수정 위치와 가져온 심볼 목록만을 저장하기 때문.
- 나머지 정보는 최종적으로 'fixups'이 고정될 위치인 DATA 세그먼트 자체에 인코딩.
- 'fixups' 위치가 서로 연결되어 있기 때문에 'chained fixups'이라고 명명함
- LINKEDIT은 첫번째 수정이 있었던 위치만 알려주고 DATA의 64bit 포인터 위치에 있는 일부 비트들은 다음 'fixups' 위치에 대한 오프셋을 포함함
- 이 안에 fixups이 'Binds'인지 'Rebases'인지 알려주는 bit도 들어있음.
- 'Binds'의 경우 나머지 bit는 심볼의 인덱스가 됨.
- 'Rebases'의 경우 나머지 bit는 이미지 내 target에 대한 오프셋이 됨.
- 'chained fixups'에 대한 런타임 지원은 iOS 13.4이상부터 존재.
chained fixups을 제대로 이해하기 위해서는 dyld가 작동하는 방식에 대해서 알아야 함.

dyld는 메인 실행 파일에서 시작.
- 앱이라고 가정하면 mach-o를 파싱하여 종속 dylib들을 찾는데, 필요한 동적 라이브러리를 찾는 것.
- 해당 dylib들을 찾아 mmap()을 수행.
- 그 다음 이들 각각에 대해 재귀적으로 mach-o 구조를 파싱하는데 필요에 따라 추가적으로 dylib를 로드하면서 진행함.
- 모든 것이 로딩되면 dyld는 필요한 모든 'Binds' 심볼을 조회하고 fixups을 수행할 때 해당 주소를 사용
- 모든 fixups이 완료되면 dyld는 이니셜라이저를 상향식으로 실행.
애플은 5년전에 이를 발표했으며 프로그램과 앱이 실행될 때마다 녹색 단계들이 매번 동일.
dylib들이 변경되지 않는 한 모든 녹색 단계는 첫번째 실행에서 캐시되고 후속 실행에서는 재사용 될 수 있음.

WWDC22에서는 dyld의 새로운 속성으로 Page-in linking을 발표.
- dyld가 모든 fixups을 실행시에 모든 dylib에 적용하는 대신 커널이 fixups을 DATA 페이지에 lazy하게 적용할 수 있음.
- mmap() 처리된 영역의 일부 페이지에서 일부 주소를 처음 사용하면 커널이 해당 페이지를 읽도록 유발하는 경우가 항상 있었음.
- 그러나 이제 DATA 페이지의 경우 커널은 해당 페이지에 필요한 fixup도 적용함.
- 10년이 넘게 dyld 공유 캐시에 있는 OS dylib에 대한 page-in linking 특수 케이스가 존재했었음.
- WWDC22부터는 이를 일반화하여 모든 사람들이 사용할 수 있도록 만듦
- 이 매커니즘은 더티 메모리와 실행 시간을 줄임.
- 이는 DATA_CONST 페이지가 깨끗하다는 의미이고 TEXT 페이지처럼 축출과 재생성이 가능해서 메모리 압박이 줄어듦
- Page-in linking은 chained-fixups로 빌드된 바이너리에만 작동.
- 왜냐하면 chained-fixups을 사용하면 대부분의 fixups 섲정보가 디스크의 DATA 세그먼트에 인코딩되고 Page-in linking 중에 커널에서 사용할 수 있기 때문
- 주의할 것은 dyld가 이 매커니즘을 launch 중에만 사용한다는 것.
- 나중에 dlopen()된 dylib들은 Page-in linking되지 못하는데, 이 경우에는 dyld는 전통적인 경로를 사용하고 fixups을 dlopen 호출 중에 적용

기존 dyld 워크플로우 다이어그램으로 돌아가자면 첫 실행에서 작업을 캐싱하고 후행 실행에서 이를 재사용함으로써 최적화하는 방향으로 진행.
2022년 이후에는 apply fixups 단계에서 fixups를 직접 수행하지 않고 lazy하게 Page-in 단계에서 커널이 하도록 만들어서 최적화 할 수 있음

동적 링킹의 모범 사례에 대해서 알아보고자 함.
즉, 동적 링크의 성능 개선을 위해 가능한 것 중에서는 기존에 dyld는 이미 동적 링킹의 많은 부분을 최적화 함.
따라서 우리가 제어할 수 있는건 dylib의 수임.

- 더 많은 dylib가 있을수록 dyld가 더 많은 로드 작업을 수행해야 하므로 더 적은 dylibs를 사용하면 더 적은 로드 작업을 수행할 수 있음.
- 정적 이니셜라이저를 사용하여 최적화 할 수 있는데, 정적 이니셜라이저에서는 I/O 또는 네트워킹을 수행하지 말 것.
- 몇 밀리초 이상 소요될 수 있는 작업은 이니셜라이저에서 수행되어서는 안됨.
- 세상이 점점 복잡지고 요구사항이 늘어나면서 코드를 라이브러리로 관리하는게 합리적.
- 목표는 동적 및 정적 라이브러리 사이의 최적의 지점을 찾는 것.
- 너무 많은 정적 라이브러리와 반복적인 빌드/디버그 사이클은 늦어짐.
- 반면에 너무 많은 동적 라이브러리와 느린 시작 시간은 사용자가 알아차릴 수 있음.
- WWDC22 이후로는 ld64의 속도를 높였기 때문에 최적의 지점이 변경되었을 수도 있음.
- 더 많은 정적 라이브러리 또는 더 많은 소스 파일을 앱에서 직접 사용하면서 여전히 동일한 시간에 빌드할 수 있음.

링킹 과정을 들여다보는 데 도움이 될 새로운 도구 2가지 발표.


- dyld_usage
- dyld가 하는 일을 추적할 수 있음.
- macOS만 실행할 수 있거나 시뮬레이터에서 앱 실행을 추적하거나 앱이 Mac Catalyst 용으로 빌드된 경우에 사용할 수 있음.


- dyld_info
- 이를 사용해 디스크와 현재 dyld 캐시 모두에서 바이너리를 검사할 수 있음.
- 이 도구에는 많은 옵션이 있지만 export 및 fixups을 보는 방법이 있음.
- -fixups 옵션은 dyld가 처리할 모든 fixups 위치와 그 타겟을 표시함.
- 파일이 이전 스타일의 'fixups'이든 새로운 'chained-fixups'이든 관계없이 출력은 동일함.
- -exports 옵션은 dylib에 있는 모든 내보내진 심볼 및 dylib의 시작 부분의 각 심볼에 대한 오프셋을 표시함.
- 이 경우 dyld 캐시에 있는 dylib인 Foundation.framework에 대한 정보를 보여줌.
- 디스크에 파일이 없지만 dyld_info 도구는 dyld와 동일한 코드를 사용하므로 찾을 수 있음.

동적 라이브러리와 정적 라이브러리이 역사와 절충 과정을 해당 내용을 통해 이해했으니 우리 앱이 수행하는 작업을 검토해보고 그 최적의 지점을 찾았는지 판단해봐야 함.
- 앱이 대형인데 빌드가 링크하는데 시간이 꽤 오래 소요된다면 더 빠른 링커가 있는 Xcode를 사용하거나
- 여전히 정적 링크의 속도도 높이고 싶다면 세가지 링커 옵션을 살펴보고 우리 앱 빌드에서 유의미한 링크 시간 개선이 있는지 확인해보기.
- iOS 13.4 이상에서는 `chained-fixups`를 활성화하여 앱 및 임베디드 프레임워크 빌드해 볼 수 있음.
- 그 다음이 iOS 16에서 더 작고 더 빠르게 실행되는지 확인
(링크)
'apple > WWDC' 카테고리의 다른 글
| Xcode 빌드에서의 병렬 처리에 대한 오해 해소(Demystify parallelization in Xcode builds) - WWDC22 (0) | 2025.12.30 |
|---|---|
| ARC in Swift: Basics and beyond - WWDC21 (0) | 2025.10.23 |
| Secure your app with Memory integrity Enforcement (메모리 무결성 강화로 앱 보호하기) (0) | 2025.10.19 |
| Elevating an app with Swift concurrency (Swift 동시성으로 앱 수준 높이기) (0) | 2025.10.19 |
| 백그라운드에서 작업 완료하기 (Finish tasks in the background) -wwdc25 (3) | 2025.07.30 |