apple/WWDC

Xcode 빌드에서의 병렬 처리에 대한 오해 해소(Demystify parallelization in Xcode builds) - WWDC22

lgvv 2025. 12. 30. 21:18

Xcode 빌드에서의 병렬 처리에 대한 오해 해소(Demystify parallelization in Xcode builds) - WWDC22

 

Xcode의 빌드 프로세스에 대해 자세히 정리

 

목차

  • Core concepts
    • 빌드의 핵심 개념 및 빌드 성능 문제를 검토하는데 도움이 되는 Xcode 도구 탐색
  • Build phase
    • 타겟 빌드 시 Xcode의 병렬화 증가 방법
  • Corss-Target builds
    • Xcode가 어떻게 많은 타겟으로 구성된 프로젝트를 구축하면서 빌드를 전체적으로 병렬로 처리하는지

 

 

 

 

 

Xcode의 빌드 시스템은 전체 프로젝트의 표현으로 호출.

모든 source file, assets, build configuration 등 실행 타겟과 같은 기타 구성을 포함.

 

 

빌드 시스템은 앱 빌드 방법에 대한 단일 정보 소스로 어떤 설정을 사용하여 어떤 도구를 호출할 지 최종적으로 앱을 만들기 위해 어떤 중간 파일을 생성할 지 알고 있음.

 

다음 단계로 빌드 시스템은 컴파일러와 같은 프로젝트의 입력 파일을 처리하는 도구를 호출.

 

 

 

Clang과 Swift 두 컴파일러 모두 링커가 앱을 나타내는 실행 프로그램을 연결하는 데 필요한 오브젝트(.o) 파일을 생성.

 

 

 

  • Swift 컴파일러
    • 입력 소스 파일을 사용해서 사용자의 의도를 캡처하고 실행 가능한 바이너리(기계어)로 변환하여 오류가 있는지 소스 코드를 확인.
    • 이 프로세스는 실패할 수 있고 실패 시 빌드가 취소되지만, 성공할 경우 각 입력에 대한 오브젝트(.o) 파일을 생성
  • 오브젝트 파일
    • 이 과정에서 오브젝트 파일은 링커를 호출하는데 사용.
  • 링커
    • 오브젝트 파일들을 결합하고 실행 파일을 생성하기 위해 외부에 연결된 라이브러리에 대한 참조를 추가

 

이 두 작업은 소비와 생산을 기반으로 종속성을 가짐.

컴파일러에 의해 생성된 오브젝트 파일은 링커에 의해 소비되며, 이로 인해서 빌드 시스템 그래프에 종속성이 만들어짐.

 

파일 콘텐츠 자체는 빌드 시스템에 중요하지 않지만 작업 간의 종속성은 중요.

빌드를 실행하는 동안에는 다른 작업의 입력을 생성하는 작업이 완료됐다는 확인이 되어야 해당 작업을 시작할 수 있음.

이 핵심 개념은 모든 종류의 작업에 유효

 


작업 A와 작업 B사이의 종속성을 보여주는 보다 일반적인 시각화 예시

 

해당 케이스의 경우에는 A는 B의 입력 중 일부 또는 전체를 생성.

컴파일링과 링킹은 전체 타겟을 빌드하기 위해 실행해야 하는 많은 작업 타입 중 일부일 뿐이므로 그래프에 좀 더 일반적인 작업을 추가.

에셋 컴파일링, 파일 복사 또는 코드 서명 같은 다른 타입의 작업(이미지의 Task C, Task D, Task E 등) 이들은 다함께 프레임워크 타겟의 빌드를 나타냄.

 

이러한 작업은 입력 및 출력을 기반으로 종속성을 정의.

따라서 Task A를 완료하면 Task B, C의 차단이 해제되고 Task B를 완료하면 Task D, E의 차단이 해제됨.

차단이 해제된 작업을 'donwstream'이라고 하며, 차단하는 작업을 'upstream'이라고 함.

 

 

많은 프로젝트에는 둘 이상의 프레임워크 타겟이 포함되므로 App과 App Extension을 나타내는 타겟으로도 확인해 보고자 함.

타겟은 프로젝트에서 서로 간의 종속성을 명시적 혹은 암시적 종속성을 통해 정의.

 

'Link Binary with Libraries' 빌드 단계에 추가되어 종속성을 정의하는 것과 같이 App은 Framework에 링크됨.

하지만, App Extension은 Framework와 종속 관계가 없음.

 

빌드 그래프를 실행할 때 서로 다른 작업엔 다른 시간이 걸리는데, 작업을 완료하는데 필요한 복잡성 수준으로 귀결됨.

입력의 크기와 필요한 계산에 따라 달라짐.

 

 

많은 파일의 컴파일링은 일반적으로 몇개의 헤더 파일 복사보다 훨씬 더 시간이 많이 걸림

이를 고려하면 위와 같은 이미지이며, 빌드 시스템이 같은 빌드를 실행할 때 종속성이 없는 작업을 먼저 실행하며 시작.

 

각각의 Task를 실행하면서 다운스트림 차단을 해제하면서 진행됨.

계획된 모든 작업이 완료될 때까지 이 프로세스를 따름.

 

 

다음 빌드에서는 입력이 변경되지 않는 작업은 빌드 시스템이 건너뛸 수 있음. (증분빌드)

변경된 입력으로 인해 Task를 다시 실행해야 하는 경우 다운스트림 작업도 재실행 해야함.

 

작업 실행의 종속성과 소요 시간은 첫 다운스트림 작업을 시작할 수 있는 시간을 말함.

따라서 이 정보를 사용하여 임계 경로를 계산할 수 있음.

 

 

이론적으로 무한한 리소스가 있을 때 빌드 실행에 필요한 가장 짧게 걸리는 시간을 의미.

해당 세션에서는 이 경로를 최적화해서 병렬화 및 확장성이 뛰어난 빌드 그래프를 생성하는 것을 알아봄.

 

더 짧은 임계 경로가 반드시 전체 빌드 시간을 단축하는건 아니지만, 빌드가 하드웨어와 함께 확장되도록 함.

빌드 임계 경로는 빌드 속도를 제한하는 요소들을 정의하며, 하드웨어에서 허용할지라도 빌드를 더 빨리 완료할 수 없음.

임계 경로 단축은 그 안에 있는 종속성을 분해하여 수행.

 

 

빌드가 어떻게 수행되었는지 살펴보고 그 빌드 실행에 대해 더 많은 이해를 하려면 실행 시간 기준의 데이터 플롯이 필요.

 

  • 너비
    • 작업의 길이를 나타냄.
  • 높이
    • 병렬로 처리되는 작업의 수를 나타냄. CPU와 메모리 사용률과 직결되진 않음.
  • 빈 공간
    • 다운스트림 작업을 막는 작업들로 인해 발생함.
  • 색상
    • 연관된 타겟

 

 

증분빌드에서는 실행된 작업만 포함되어서 오래 실행되는 작업을 발견할 수 있도록 도움.

특히, 이 빌드 중에 실행될 것으로 예상되지 않았을 수 있는 것들을 발견할 수 있도록 함.

 

 

 

해당 타겟의 실행 파일을 위해 성공적으로 빌드하기 위해 Xcode는 이 노드의 자식이 나타낸 모든 작업을 실행.

현재 빌드 로그에서 All로 되어 있으면 증분 빌드에서 다시 실행할 필요가 없던 이전 빌드의 작업도 표시됨.

하지만 Recent로 변경할 경우 실제로 실행된 작업만 표시되며, 건너 뛴 작업은 모두 숨겨짐.

 

 

이상적인 타임라인은 가능한 빈 공간이 적게 세로로 채워짐.

이렇게 하는 것이 빌드 그래프가 가장 잘 확장되고, 빌드가 빨라지며 이에 따라 하드웨어도 빨라짐.

 

 

Xcode가 개별 타겟을 정의하고 빌드하는 방법 및 병렬화를 증가시키는 방법에 대해서 살펴보고자 함.

타겟을 구성할 때, Build Phases는 해당 타겟의 제품 생산을 위해 수행해야 하는 작업을 설명.

 

컴파일 해야하는 에셋 및 헤더 또는 리소스와 같은 복사해야 하는 파일 그리고 링크되는 라이브러리나 실행되는 스크립트를 포함.

많은 빌드 단계는 다른 빌드 단계의 입출력 작업을 설명하며, 그들 사이에 종속성을 생성.

 

 

예를들어 타겟의 소스 파일은 링크되기 전에 컴파일이 되어야 함.

하지만 모든 빌드 단계에 적용되는 것은 아님.

 

각 빌드 단계의 작업들을 일렬로 실행하는 대신 빌드 시스템은 빌드 단계의 입력과 출력을 고려해 병렬로 실행이 가능한지 결정.

예를들어 컴파일과 리소스 복사는 병렬로 실행할 수 있는데, 왜냐하면 서로 다른 쪽의 출력에 의존하지 않기 때문임.

 

하지만, 링킹은 컴파일을 따라야 하는데, 왜냐하면 컴파일 단계에서 생성된 오브젝트(.o) 파일에 의존해야 하기 때문.

 

 

이번에는 'Run Script' 빌드 단계가 포함된 타겟을 확인.

다른 빌드 단계와 달리 스크립트 단계의 입출력은 Target Editor에서 수동으로 구성해야 함.

결과적으로 빌드 단계에서 데이터 경쟁이 발생하지 않도록 연속적으로 스크립트를 한번에 하나씩 실행.

 

 

만약 타겟의 스크립트가 종속성 분석을 기반으로 실행되고 입력 및 출력의 전체 목록을 지정하도록 구성된 경우 

`FUSE_BUILD_SCRIPT_PHASES`를 YES로 설정하여 빌드 시스템이 병렬로 실행 시도를 해야함을 나타낼 수 있음.

 

 

스크립트를 병렬로 실행할 경우 빌드 시스템은 지정된 입력과 출력에 의존해야 함.

따라서 실행 단계에서 불완전한 입력 또는 출력 목록은 디버깅하기 어려운 매우 문제로 데이터 경쟁 문제로 나타날 수 있음.

 

 

Xcode는 사용자 스크립트 샌드박싱을 지원해 각 스크립트 단계의 종속성을 정확하게 선언할 수 있도록 함.

샌드박싱은 Shell Script가 실수로 소스파일 및 중간 빌드 객체에 접근하는 것을 차단하는 선택적인 기능.

 

이러한 항목이 명시적으로 선언되지 않는 한 읽고 및 쓰기를 제한하며, 스크립트가 샌드박스를 위반하면 0이 아닌 종료 코드와 함께 빌드에 실패하게 됨

 

 

명시적으로 작성하여 이 문제를 해결하고 선언된 입출력 이외의 파일에 실수로 접근하지 않도록 함.

 

 

 

위와 같은 스크립트의 예시를 보자.


스크립트 A (체크섬 계산): source.txt 파일의 내용을 읽어 checksum.txt 파일을 생성

스크립트 B (HTML 생성): checksum.txt 파일을 읽어 HTML 파일에 내용을 삽입

 

 

 

`FUSE_BUILD_SCRIPT_PHASES`가 켜져있을 때 병렬로 실행하는데, Xcode가 두 종속성 관계를 제대로 추론하지 못함.

 

스크립트 B가 A보다 먼저 실행되면 checksum.txt 파일이 없어 빌드가 실패하거나, A가 실행 중일 때 B가 오래된 checksum.txt 파일을 읽어 조용히 버그를 만들어낼 수 있습니다.

 

ENABLE_USER_SCRIPT_SANDBOXING를 YES로 설정한 상태에서는 읽으려는 단계에서 실패하게 되고, 명시적인 실패를 통해 누락된 종속성을 명확하게 찾아낼 수 있음.

 

 

Xcode의 Build Settings 혹은 xcconfig를 통해서 설정을 YES로 줄 수 있음.

 

 

 

 

sandbox shell scripts를 활용하면 올바른 종속성 정보를 통해 증분빌드를 가능하게 하는 올바른 종속성 정보를 가질 수 있음.

왜냐하면, 입출력 파일의 변화가 없다는 확신을 통해 빌드 과정을 건너뛸 수 있기 때문이며, 그렇지 않은 경우에는 Xcode는 스크립트를 다시 실행함. 

 

샌드박싱을 통해 올바르게 정의된 종속성 정보를 통하여 `FUSE_BUILD_SCRIPT_PHASES`와 함께 병렬로 실행할 수도 있음.

 


Xcode가 Swift Target 간의 종속성을 사용하여 빌드에서 최대 병렬 처리량을 추출하는 방법을 살펴보고 프로젝트 구조 및 구성이 빌드 시간에 미치는 영향도 알아보고자 함.

 

 

프로젝트를 구성하는 계층 구조에는 여러 수준이 존재

예를들어 로컬 라이브러리 모음에 의존하는 App Target은 의미론적 경계를 다라 여러 타겟 및 여러 프레임워크로 분할됨.

각 타겟은 다양한 빌드 단계와 절차들을 포함하여 다른 타겟 빌드 단계로 그리고 빌드 단계로부터 파일 의존성을 생성하고 사용함.

 

프로젝트가 커지면서 작업 그래프의 크기와 복잡성도 커짐

 

 

 

 

 

Xcode 빌드 시스템이 이런 계층 구조를 평면화하면서 빌드를 여러 작업들로 분할하고, 이런 작업들은 모든 타겟의 빌드 단계에 해당함.

Swift 타겟에 특화된 한 종류의 작업은 컴파일이고, Swift 타겟의 소스 코드를 바이너리 제품으로 빌드하는 것은 일반적으로 빌드 계획이나

 

컴파일 및 연결을 위한 많은 하위 작업을 포함하는 복잡한 작업으로 이러한 작업의 Swift Driver라는 Xcode toolchain에 위임.

드라이버는 타겟 소스 코드에 필요한 컴파일러 및 링커 호출을 구성하는 시기와 방법에 대한 정보를 가지고 있음.

 

Swift 코드를 포함하는 모든 타겟은 배포 단위인 모듈에도 해당됨.

이 타겟의 공개 인터페이스를 캡처하는 바이너리 모듈 파일은 다운스트림 타겟이 컴파일을 시작하는데 필요한 빌드 product임.

 

 

Swift Driver는 최적화 기회를 최대화하도록 하는 하나의 컴파일러 작업을 예약.

필요한 컴파일 노력을 병렬로 실행할 수 있는 더 작은 하위 작업으로 나눔.

이 중 일부는 증분 빌드에서 다시 실행할 필요가 없음.

 

Swift 모듈을 생성하려면 각 컴파일 작업의 부분적인 중간 product를 병합하는 추가 단계가 필요함.

타겟에 있는 소스 파일의 수가 많다면 개별 파일은 일괄 컴파일 하위 작업에 할당될 수 있음.

 

 

빌드 로그는 일괄 컴파일 작업에 할당되는 소스 파일을 강조 표시하고 각 파일의 진단에 대해 별도의 항목을 사용

다양한 소스 파일에 걸쳐 타겟의 빌드를 병렬화할 수 있는 것은 더 빠르고 작은 증분빌드에 중요하기 때문에 디버그 빌드가 증분 컴파일 모드 설정을 사용하는지 확인. 

 

 

 

Swift의 타겟 의존성은퍼블릭 인터페이스를 캡처하는 바이너리 모듈 파일을 제공함으로써 해결.

이런 의존성 관계를 해결하면 위와 같은 순서로 이어짐.

각 타겟에 대한 최상위 Swift Driver 작업 및 각 타겟의 개별 하위 작업이 타임라인에 나타남.

 

 

Xcode 14 이전의 빌드 시스템은 이원화 된 구조.

Xcode의 빌드 시스템은 전체적인 빌드 단계를 관리했고, Swift 코드 컴파일은 Swift 드라이버라는 별도의 도구에서 독립적으로 관리.

 

 

 

기존에는 두 스케줄러가 각자 시스템 CPU 코어를 최대한 활용하려다 보니 서로 경쟁하며 리소스를 비효율적으로 사용하는 CPU 수를 초과해서 사용하는 문제가 발생

Xcode 14에서는 빌드 시스템과 컴파일러가 완전히 통합되어서 중앙 관리자가 통합 관리함.

중앙 관리자는 사용 가능한 리소스 만큼만 사용하도록 보장.

 

 

 
Xcode14 이후부터 스케줄러 기본 값은 사용 가능한 작업 즉, 종속성이 충족되어서 이동할 준비가 된 작업은 8개의 사용 가능한 실행 슬롯 중 하나에 할당.

코어 수가 많을수록 많은 시스템에서는 더 많은 동시 작업을 수행할 수 있음.

 

 

하위 작업 컴파일의 부분적 결과들은 타겟의 최종 모듈 제품에 병합.

제품이 사용 가능하게 되면, 다운스트림 타겟이 컴파일을 시작할 수 있음.

 

 

 

Xcode 14 및 Swift 5.7에서는 새롭게 타겟 모듈의 구성이 모든 프로그램 소스 파일에서 직접 별도의 모듈 emit-module 작업에서 수행.

이는 타겟 종속성이 emit-module 작업이 완료되는 즉시 다른 컴파일러 작업을 기다리지 않고 컴파일을 시작할 수 있음을 의미

 

 

빌드 시간을 전체 프로젝트로 확장해서 보면 전반적으로 비슷한 양의 작업을 수행하고 있지만 빌드 시스템은 컴퓨터의 리소스를 더 효율적으로 사용할 수 있음.

따라서 종종 더 빌드를 더 빠르게 마무리할 수 있음.

 

빌드 시스템이 Swift 빌드 시에 수행할 수 있는 두 번째 Cross-Target 최적화 기능인 즉시 링킹(Eager Linking)이 존재.

 

링킹 과정에도 유사한 최적화가 적용되는데 다운스트림 타겟의 링커가 업스트림 타겟의 최종 링크 Product를 기다려야 했음.

먼저 생성되는 심볼 목록이 담긴 텍스트 기반 stub 파일에만 의존하면 되므로 링킹을 즉시 시작할 수 있음.

의존성의 대상이 무거운 최종 결과물에서 가벼운 텍스트 파일로 바뀌면서 링킹 작업이 훨씬 앞당겨짐.

 

 

 

이 경우 타겟 B가 타겟 A를 연결하기 때문에 타겟 B의 링크 작업은 타겟 A의 링크된 출력이 생성되고 자체 컴파일 작업이 완료될 때까지 기다려야 함. 

 

 

 

즉시 링킹을 사용하면, 타겟 B의 링크 작업이 타겟 A의 emit-module 작업에 의존할 수 있고, 그 결과 타겟 B는 링킹을 빌드 초기에 시작할 수 있음.

타겟 A와 병렬로 실행하며 임계 경로(Critical Path)를 단축할 수 있음.

 

 

종속성이 링크된 제품에 의존하는 대신 이전 빌드 프로세스에서 emit-module 작업에 의해 생성된 텍스트 기반 동적 라이브러리 스텁에 의존.

이 스텁에는 링크된 제품에 심볼 목록이 포함되어 있음.

 

 

Xcode 빌드 설정을 사용해 이 최적화를 활성화할 수 있음. Xcode 빌드 설정을 사용해 이 최적화를 활성화 할 수 있고

즉시 링킹은 의존성 항목에 의해 동적으로 링킹된 모든 순수 Swift 타겟에 적용

 


Xcode 14의 빌드 시스템 개선은 단순한 속도 향상이 아니라, 병렬화를 극대화하고, 개발자에게 빌드 과정을 투명하게 보여주며, 리소스를 가장 효율적으로 사용하도록 설계된 근본적인 구조 변화.

 

이제 우리는 빌드 시간을 막연히 기다리는 대신, 새로운 도구와 지식을 활용해 직접 분석하고 개선할 수 있게 되었고, 이 글에서 소개된 지식을 바탕으로 불필요한 순차 실행 스크립트는 없는지, 타겟 간 의존성이 비효율적으로 구성되지는 않았는지 점검.

 

 

 

 

(링크)

https://www.youtube.com/watch?v=yo8_luxkSXY&t=4s