[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/Swift

[WWDC22] Link fast: Improve build and launch times

inu 2024. 4. 8. 20:57
반응형

안녕하세요 inu입니다. 오랜만에 포스팅을 작성하네요 :)

 

오랜 숙원 사업이었던 'Link fast: Improve build and launch times' 감상을 드디어 마쳤습니다. 개인적으로 조사한 정보들도 포함해 글로 정리해봤습니다. 중간중간 CS 관련된 내용도 나와서 이해하기가 어려울 수 있는데, 정적 라이브러리와 동적 라이브러리의 개념과 동작방식에 대해서 정도만 이해해도 충분히 얻어가는 세션이었다고 생각합니다. 저처럼 컴파일 및 모듈 관련 지식이 부족했던 분들에게 도움이 되길 바랍니다. 

 

사전정보

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

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

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

  • Static Linking : 앱을 빌드할 때 발생, 빌드시간과 앱의 최종 크기에 영향을 줍니다.
  • Dynamic 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

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

Static Linking best practices

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

 

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

-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)를 하지않도록 만드는 옵션입니다. 일반적으로 중복제거는 최종 실행 파일을 상당히 줄일 수 있어 유용하게 사용됩니다. 하지만 중복제거는 고비용 알고리즘을 필요로 하기 때문에 빌드시간에 악영향을 줍니다. 따라서 최종 실행 파일보다는 빌드 속도가 더 중요한 디버깅환경에서는 해당 옵션을 사용하는 것이 도움이 됩니다.

What is dynamic linking?

이것이 기존 라이브러리 구조입니다. 많은 정적 라이브러리 파일과 오브젝트 파일이 모여 최종적으로 실행가능한 프로그램이 됩니다. 이는 기능이 늘어나면 늘어날수록 점점 앱의 실행 크기가 커지는 구조입니다.

아카이브 도구(ar) 대신 링커(ld)를 사용해 최종 라이브러리가 실행가능한 바이너리 형태(graphics.dylib)가 됩니다. 이것이 90년대에 대두된 동적 라이브러리의 시작이었습니다.

라이브러리에서 최종 실행 프로그램으로 코드를 복사하는 것이 아니라, 일종의 약속(Promise)를 합니다. 동적 라이브러리에서 사용될 심볼 및 라이브러리 경로를 기록해놓는 것입니다. 동적 라이브러리는 최종 실행 프로그램에 포함되어 있지 않고 외부에 저장되어 있습니다. 실행 프로그램에서 이를 필요로 할 경우 메모리에 올리고 접근하여 사용하는 것입니다.

About Dynamic linking

동적 라이브러리는 여러 프로세스에서 동시에 사용할 수 있다는 장점이 있습니다. 운영 체제는 동일한 동적 라이브러리에 대해, 그 라이브러리를 메모리에 로드하는 모든 프로세스 사이에서 해당 라이브러리의 메모리 페이지(데이터가 저장되는 메모리의 단위)를 재사용합니다.

다만 동적 라이브러리를 사용할 경우 특정 프로그램 실행시 하나의 프로그램 파일만 로드하는 것이 아니기 때문에 실행 시간을 둔화시킵니다. 동적 라이브러리가 많으면 많을수록 실행시간은 더욱 늦춰지겠죠.

 

또 메모리에 저장되어야만 하는 '더티 페이지'가 많아집니다. 각 프로세스가 동적 라이브러리를 사용할 때, 데이터 세그먼트는 공유되지 않고 프로세스별로 독립적입니다. 이는 한 프로세스에서 발생한 데이터 변경이 다른 프로세스의 라이브러리 데이터에 영향을 주지 않음을 의미합니다. 이 독립성은 각 프로세스 내에서 데이터를 변경할 때마다 더티 페이지를 증가시키고, 이는 곧 메모리 사용량 증가와 성능 저하를 초래합니다. 특히, 시스템이 메모리 압박을 받을 때 스왑 영역으로 데이터를 이동시켜야 하는데, 더티 페이지는 이 과정에서 디스크 I/O 부하를 증가시킬 수 있습니다.

 

또한 빌드 시 실행 파일에 기록되는 약속(Promise)을 이행할 수 있는 동적 링커가 필요해집니다. 실행 가능한 바이너리는 여러 세그먼트로 분리되는데, 최소한 TEXT, DATA, LINKEDIT 세그먼트로 분리됩니다. 각각의 세그먼트는 다른 기능을 가집니다.

  • TEXT 세그먼트: 이 부분은 실행 파일의 코드를 포함하고 있으며, 컴파일된 기계어 명령어들을 포함합니다. 일반적으로 읽기 전용이며, 이 영역의 데이터는 실행 중에 변경되지 않습니다. '실행' 권한을 가집니다.
  • DATA 세그먼트: 전역 변수와 정적 변수와 같은 초기화된 데이터를 포함합니다. 이 세그먼트는 실행 중에 변경될 수 있는 데이터를 저장합니다. 런타임 시, 여기에는 실제 메모리 주소를 가리키는 포인터들이 포함될 수 있습니다.
  • LINKEDIT 세그먼트: 심볼 테이블, 문자열 테이블, 리로케이션 정보 등 링킹에 필요한 메타데이터를 포함합니다. 이 정보는 프로그램이 실행될 때 동적 링커에 의해 사용되며, 주소 바인딩과 같은 작업을 수행하는 데 필요합니다.

Wired up & Fix ups

런타임시 동적 링커는 각 세그먼트의 권한을 기반으로 실행 파일들을 메모리에 매핑(mmap)해야합니다. 가상 메모리에 이를 매핑해놓고 실질적인 접근이 있을 때 실질적인 메모리에 로드되게 됩니다.

여기에 더해 프로그램이 올바르게 실행되기 위해서는 추가적인 단계인 'wired up(연결)' 과정이 필요합니다. 우리 프로그램에서 동적 라이브러리의 실질적 메모리 주소를 가리킬수 있도록 하는 과정입니다. '연결'의 과정을 이해하기 위해 'fix ups(수정)'의 개념에 대해 알아봅시다.

C 표준 라이브러리(동적 라이브러리)에 위치한 malloc()을 호출하는 케이스를 예시로 '수정' 과정에 대해 알아봅시다. 이 모든 과정은 앱 실행시간에 발생하는 '동적 링킹'에 이루어집니다. 동적링커(dyld)는 DATA 세그먼트에 있는 포인터를 업데이트하여 malloc 함수의 실제 메모리 주소를 기록해놓습니다. malloc 함수의 상대주소는 프로그램이 빌드될 때 알 수 없습니다. 따라서 정적 링커는 컴파일시 malloc에 해당하는 부분을 '스텁'이라는 일종의 임시 코드 조각으로 대체해 놓습니다. 그리고 링커는 'bl' 명령어를 통해 스텁을 호출합니다. 스텁은 동적 링커와 상호작용하여 malloc의 실제 주소를 찾아냅니다. DATA 세그먼트에 저장되어 있는 포인트를 로드하여 malloc 함수의 위치로 점프합니다. 여기서 확인할 부분은 런타임에서 TEXT 세그먼트는 변경되지 않습니다. 변경되는 부분은 단지 동적 링커에 의한 DATA에 대한 포인터 설정 뿐입니다.

동적 링커가 수행하는 '수정'에 대해서 더 자세히 알아봅시다. '수정'에는 두가지 타입이 존재합니다. 두가지 타입 모두 수행해야 프로그램 혹은 라이브러리가 정상적으로 동작할 수 있습니다.

리베이스

첫번째 타입은 리베이스입니다. (앞선 예시에서 DATA 세그먼트에 있는 포인터를 업데이트하는 과정)

  • 실행 파일이나 dylib(동적 라이브러리) 내부에서 다른 부분을 가리키는 포인터가 있을 경우, ASLR(실행 파일이나 라이브러리를 메모리에 로드할 때마다 그 위치를 무작위로 변경하는 보안 기능)로 인해 이 포인터가 가리키는 대상의 실제 메모리 주소가 빌드 시점에는 알 수 없습니다.
  • dyld는 실행 시에 ASLR에 따라 dylib를 메모리의 임의의 주소에 로드합니다. 이 때문에 빌드 시점에 설정된 내부 포인터들은 더 이상 유효한 메모리 주소를 가리키지 않게 됩니다.
  • 실행 파일이나 dylib가 메모리에 로드된 후, dyld(동적 링커)는 이 내부 포인터들을 현재 로드된 주소에 맞게 조정해야 합니다. 이 과정을 '리베이스'라고 하며, 포인터들이 올바른 메모리 위치를 가리키도록 합니다.
  • 실행 파일이나 dylib의 LINKEDIT 세그먼트에는 리베이스 작업에 필요한 정보가 저장됩니다. 이는 포인터들이 원래 가리키려고 했던 대상 주소(예를 들어, dylib가 메모리 주소 0에서 시작한다고 가정했을 때의 주소)를 포함합니다.
  • dyld는 LINKEDIT 세그먼트의 정보를 참조하여, 각 내부 포인터가 현재의 실제 메모리 위치를 올바르게 가리키도록 조정합니다. 이는 프로그램이나 라이브러리의 각 부분이 서로 올바르게 '연결'되어 작동하도록 보장합니다.

바인드

두번째 타입은 바인드입니다. (앞선 예시에서 malloc의 실제 주소를 찾아내는 과정)

  • 프로그램에서 특정 기능을 사용하기 위해, 예를 들어 메모리 할당을 위한 'malloc' 함수를 호출하려고 할 때, 그 함수를 상징적 이름인 심볼(여기서는 '_malloc')으로 참조합니다. 이는 프로그램 코드가 특정 메모리 주소를 직접 참조하는 대신, 심볼을 사용하여 참조하는 방식입니다.
  • 실행 파일이나 동적 라이브러리의 LINKEDIT 세그먼트에는 심볼의 이름과 그에 대한 다양한 정보가 저장됩니다. 동적 링커(dyld)는 실행 시 이 정보를 참조하여 심볼의 실제 메모리 주소를 찾아냅니다. 예를 들어 'libSystem.dylib'에 정의된 '_malloc' 심볼의 실제 메모리 주소를 찾아내는 방식입니다.
  • dyld가 심볼의 실제 메모리 주소를 찾으면, 그 주소를 프로그램이나 라이브러리 내에서 해당 심볼을 참조하는 위치에 연결(바인드)합니다. 이를 통해 프로그램은 실행 시점에 심볼의 실제 위치를 알고, 올바르게 기능을 호출할 수 있게 됩니다.

리베이스(Rebase)와 바인드(Binding) 모두 프로그램이나 라이브러리가 메모리에 올바르게 로드되고, 실행 중에 필요한 모든 코드와 데이터에 정확하게 접근할 수 있도록 하는 필수적인 과정입니다.

애플은 여기서 새로운 수정 방법인 'chained fix ups'를 발표합니다.

  • 기존에는 수정이 필요한 모든 위치의 정보를 LINKEDIT 세그먼트에 저장해야 했습니다. 하지만 'chained fixups' 방식에서는 각 DATA 페이지의 첫 번째 수정 위치와 해당 페이지에서 필요한 심볼 목록만을 LINKEDIT에 저장합니다. 이로 인해 LINKEDIT 세그먼트의 크기가 줄어듭니다.
  • 나머지 수정 정보는 DATA 세그먼트 자체에 직접 인코딩됩니다. 이는 수정이 최종적으로 적용될 메모리 위치에 대한 정보를 포함합니다.
  • 'Chained fixups'의 핵심은 수정 위치가 서로 연결되어 있다는 것입니다. LINKEDIT는 첫 번째 수정 위치만 알려주고, DATA 세그먼트 내의 각 64비트 포인터에는 다음 수정 위치로의 오프셋이 포함됩니다.
  • DATA 세그먼트에 저장된 정보는 또한 해당 수정이 바인드인지 리베이스인지를 구분합니다. 이는 포인터의 일부 비트를 사용하여 표시됩니다. 바인드인 경우 나머지 비트는 필요한 심볼의 인덱스를 나타냅니다. 리베이스인 경우 나머지 비트는 해당 이미지 내의 대상 주소로의 오프셋을 나타냅니다.
  • 이 방식은 수정 정보를 보다 효율적으로 저장하고 처리할 수 있게 해주어, 동적 링킹 과정을 최적화합니다. 결과적으로 LINKEDIT 세그먼트의 크기 감소되고 수정 위치가 효율적으로 인코딩되어 프로그램 로드 시간을 단축시키고 전반적인 시스템 성능을 향상시킵니다.

이러한 chained fix ups 방식은 iOS 13.4 이상부터 지원됩니다.

Dynamic Linking Process

다음은 동적 링킹의 과정을 세부적으로 나타낸 것입니다.

  • parse mach-o : dyld는 애플리케이션의 실행 파일을 분석하여 시작합니다. (cf. Mach-O는 macOS와 iOS에서 사용되는 실행 파일 포맷입니다.)
  • find dependent dylibs : 애플리케이션 실행에 필요한 다른 dylib(동적 라이브러리)들의 목록을 확인합니다.
  • mmap segments : mmap() 시스템 호출을 사용하여 필요한 세그먼트(코드, 데이터 등)를 메모리에 매핑합니다.
  • lookup symbols : 애플리케이션에 필요한 심볼(함수나 변수 등)의 주소를 찾아냅니다.
  • apply fixups : 리베이스와 바인드 과정을 수행하여, 심볼들이 올바른 메모리 주소를 가리키도록 만듭니다.
  • run initializers : 애플리케이션과 라이브러리 내부에 정의된 초기화 코드를 실행합니다.

2017년에 애플은 이중 parse mach-o, find dependent dylibs, lookup symbols 과정을 캐싱하여 첫번째 실행이후엔 재사용될 수 있도록 처리하여 개선했습니다.

Recent dyld improvements

이번에 또 새로운 개선이 있었습니다. dyld의 속성인 '페이지 인 링킹(page-in linking)'입니다. 페이지 인 링킹은 메모리에 로드된 애플리케이션의 데이터 세그먼트에 필요한 수정 사항들을 게으르게(즉, 필요한 시점에) 적용하는 기능입니다. 따라서 그림에서는 파란색(apply fixups) 단계의 개선에 해당하겠죠.

  • 전통적으로, dyld는 애플리케이션을 실행할 때 필요한 모든 바인드(Binding)와 리베이스(Rebase) 수정 사항을 즉시 적용했습니다. 하지만 'page-in linking' 기능을 사용하면, 이런 수정 사항을 모든 dylib에 미리 적용하는 대신, 실제로 해당 데이터 페이지에 접근할 때까지 기다렸다가 수정을 적용합니다.
  • 애플리케이션과 라이브러리의 세그먼트들은 mmap() 시스템 호출을 통해 메모리에 매핑됩니다. 사용자가 특정 페이지에 처음 접근할 때 페이지 폴트가 발생하고, 이 때 커널이 필요한 페이지를 메모리로 읽어들입니다.
  • 'page-in linking'에서는 데이터 페이지가 메모리로 로드될 때 커널이 페이지에 필요한 수정을 적용합니다. 이것은 이전에는 dyld가 수행했던 작업을 커널이 대신하는 것을 의미하며, 메모리 사용을 최적화하고 실행 시간을 단축시키는 데 기여합니다. (동적 링커는 앱 최초 실행시에 동작하기 때문에 이 작업을 수행할 수 없습니다.)
  • 런타임 중 동작하는 이러한 동작은 사용자가 인지할 수 없을 정도로 짧기 때문에 사용성에 악영향을 주지도 않습니다.
  • 'page-in linking'을 사용함으로써 데이터 페이지(DATA_CONST)는 변경되지 않은 상태(즉, "깨끗한" 상태)를 유지하게 됩니다. 이는 페이지를 메모리 압력이 있을 때 추출하고 필요할 때 다시 가져오는(TEXT 페이지와 유사한) 방식으로 처리할 수 있다는 것을 의미합니다.
  • 'page-in linking'은 'chained fixups'(연결 수정)을 사용하여 빌드된 바이너리에서만 사용할 수 있습니다. 'chained fixups'는 대부분의 수정 정보를 디스크의 데이터 세그먼트에 저장하기 때문에, 이 정보를 페이지-인 링크 과정에서 쉽게 사용할 수 있습니다.
  • 주의할점은 런타임 도중 dlopen()을 통해 로드된 dylib들은 이 매커니즘을 활용하지 못합니다. (dlopen : 런타임 중 동적라이브러리를 메모리에 로드하도록 도와주는 UNIX 기반 시스템의 동적 링킹 라이브러리(Dynamic Linking Library, DLL)에서 제공하는 함수 중 하나)

Dynamic Linking best practices

앞선 설명에서 보았듯, 동적 링커는 성능 개선이 이미 많이 된 상태입니다. 따라서 우리가 제어할 수 있는 것은 동적 라이브러리의 개수입니다. 더 많은 동적 라이브러리가 존재할수록 실행시간이 더 많이 늘어날 것을 분명하게 인지하고 있어야합니다.

 

또한 전역/정적 변수를 초기화할 때 사용되는 정적 이니셜라이저에서는 I/O 혹은 네트워킹 등 많은 시간이 소요될 수 있는 작업을 수행해서는 안됩니다. 정적 이니셜라이저는 항상 main 함수 이전에 호출되기 때문에 여기서 복잡한 작업이 발생하면 실행시간에 안좋은 영향을 주겠죠.

 

결국 중요한 것은 빌드시간과 실행시간의 영향범위를 잘 고려해서 중간지점을 잘 찾아내는 것입니다.

New tools

dyld_usage : 동적 링커가 하는 일을 추적할 수 있습니다.

dyld_info : 디스크와 현재 동적 링커 캐시 모두에서 바이너리를 검사할 수 있습니다.

  • fixup 옵션 : 동적 링커가 처리할 모든 수정 위치와 대상 표시
  • exports 옵션 : 동적 라이브러리에 있는 모든 내보내기 심볼 및 동적 라이브러리의 시작 부분의 각 심볼에 대한 오프셋을 표시
반응형