병렬 컴파일
- 코드 생성 부분은 기본적으로 동시에 실행됩니다.
-C codegen-units=n옵션을 사용하여 동시 작업의 수를 제어할 수 있습니다. - 타입 검사, 빌림 검사, MIR 최적화처럼 HIR 로워링 이후부터 코드 생성까지의 부분은 nightly 버전에서 병렬화되어 있습니다. 현재 이 부분들은 기본적으로 직렬로 실행되며, 사용자가
-Z threads = n옵션을 사용하여 수동으로 병렬화를 활성화해야 합니다. - 어휘 파싱, HIR 로워링, 매크로 확장 같은 다른 부분은 여전히 직렬 모드로 실행됩니다.
코드 생성
단형화(monomorphization) 과정에서 컴파일러는 생성할 모든 코드를 코드 생성 단위라고 하는 더 작은 청크로 나눕니다. 그런 다음 이 청크들은 병렬로 실행되는 독립적인 LLVM 인스턴스들이 생성합니다. 마지막으로 링커가 실행되어 모든 코드 생성 단위를 하나의 바이너리로 결합합니다. 이 과정은 rustc_codegen_ssa::base 모듈에서 이루어집니다.
자료 구조
병렬 컴파일러에서 사용하는 기반 스레드 안전 데이터 구조는 rustc_data_structures::sync 모듈에서 찾을 수 있습니다. 이 자료 구조는 parallel-compiler가 true인지에 따라 다르게 구현됩니다.
| 자료 구조 | 병렬 | 비병렬 |
|---|---|---|
| Lock<T> | (parking_lot::Mutex<T>) | (std::cell::RefCell) |
| RwLock<T> | (parking_lot::RwLock<T>) | (std::cell::RefCell) |
| ReadGuard | parking_lot::RwLockReadGuard | std::cell::Ref |
| MappedReadGuard | parking_lot::MappedRwLockReadGuard | std::cell::Ref |
| WriteGuard | parking_lot::RwLockWriteGuard | std::cell::RefMut |
| MappedWriteGuard | parking_lot::MappedRwLockWriteGuard | std::cell::RefMut |
| LockGuard | parking_lot::MutexGuard | std::cell::RefMut |
-
이러한 스레드 안전 자료 구조는 컴파일 과정 곳곳에서 사용됩니다. 이 때문에 락 경합이 발생할 수 있으며, 스레드 수가 4개를 넘으면 성능이 저하됩니다. 그래서 이러한 자료 구조가 어떻게 사용되는지 점검합니다. 점검 결과에 따라 공유 상태 사용을 줄이도록 리팩터링하거나, 불변 조건의 세부 사항과 원자성, 락 순서를 설명하는 문서를 작성해 계속 유지합니다.
-
한편, 컴파일 중의 다른 불변 조건 가운데 병렬 컴파일에서 성립하지 않을 수 있는 것이 무엇인지는 아직 파악해야 합니다.
WorkerLocal
WorkerLocal은 병렬 컴파일러를 위해 구현된 특수한 자료 구조입니다. 이 자료 구조는 스레드 풀의 각 스레드에 대한 워커 로컬 값을 보관합니다. 워커 로컬 값에는 그 값이 생성된 스레드 풀에서 Deref impl을 통해서만 접근할 수 있습니다. 그렇지 않으면 패닉이 발생합니다.
WorkerLocal은 병렬 환경에서 Arena 할당자를 구현하는 데 사용되며, 이 할당자는 병렬 쿼리에서 매우 중요합니다. 구현은 rustc_data_structures::sync::worker_local 모듈에 있습니다. 그러나 비병렬 컴파일러에서는 (OneThread<T>)로 구현되며, 이때 T에는 Deref::deref를 통해 직접 접근할 수 있습니다.
병렬 반복자
rayon 크레이트가 제공하는 병렬 반복자를 사용하면 병렬성을 쉽게 구현할 수 있습니다. 현재 병렬 컴파일러 구현에서는 작업을 병렬로 실행하기 위해 rayon의 커스텀 포크를 사용합니다.
일부 반복자 함수는 parallel-compiler가 true일 때 루프를 병렬로 실행하도록 구현되어 있습니다.
함수(Send와 Sync는 생략) | 소개 | 소속 모듈 |
|---|---|---|
| par_iter<T: IntoParallelIterator>(t: T) -> T::Iter | 병렬 반복자를 생성합니다 | rustc_data_structure::sync |
| par_for_each_in<T: IntoParallelIterator>(t: T, for_each: impl Fn(T::Item)) | 병렬 반복자를 생성하고 각 요소에 대해 for_each를 실행합니다. | rustc_data_structure::sync |
| Map::par_body_owners(self, f: impl Fn(LocalDefId)) | 크레이트의 모든 HIR 소유자(owner)에 대해 f를 실행합니다. | rustc_middle::hir::map |
| Map::par_for_each_module(self, f: impl Fn(LocalDefId)) | 크레이트의 모든 모듈과 하위 모듈에 대해 f를 실행합니다. | rustc_middle::hir::map |
| ModuleItems::par_items(&self, f: impl Fn(ItemId)) | 모듈의 모든 아이템에 대해 f를 실행합니다. | rustc_middle::hir |
| ModuleItems::par_trait_items(&self, f: impl Fn(TraitItemId)) | 모듈의 모든 트레이트 아이템에 대해 f를 실행합니다. | rustc_middle::hir |
| ModuleItems::par_impl_items(&self, f: impl Fn(ImplItemId)) | 모듈의 모든 impl 아이템에 대해 f를 실행합니다. | rustc_middle::hir |
| ModuleItems::par_foreign_items(&self, f: impl Fn(ForeignItemId)) | 모듈의 모든 외부(foreign) 아이템에 대해 f를 실행합니다. | rustc_middle::hir |
컴파일러에는 이러한 함수를 사용하여 병렬화할 수 있는 루프가 많이 있습니다. 2022년 8월 기준으로, 병렬 이터레이터 함수가 사용된 경우는 다음과 같습니다:
| 호출자 | 시나리오 | 피호출자 |
|---|---|---|
| rustc_metadata::rmeta::encoder::prefetch_mir | 나중에 메타데이터 인코딩에 필요한 쿼리를 미리 가져옵니다. | par_iter |
| rustc_monomorphize::collector::collect_crate_mono_items | 제네릭이 아닌 항목에서 도달할 수 있는 단형화된 항목을 수집합니다 | par_for_each_in |
| rustc_interface::passes::analysis | match 문의 유효성을 검사합니다 | Map::par_body_owners |
| rustc_interface::passes::analysis | MIR 빌림 검사 | Map::par_body_owners |
| rustc_typeck::check::typeck_item_bodies | 타입 검사 | Map::par_body_owners |
| rustc_interface::passes::hir_id_validator::check_crate | hir의 유효성을 검사합니다 | Map::par_for_each_module |
| rustc_interface::passes::analysis | 루프 본문, 속성, naked 함수, 불안정 ABI, const 본문의 유효성을 검사합니다 | Map::par_for_each_module |
| rustc_interface::passes::analysis | MIR의 활성 검사 및 내재적 검사 | Map::par_for_each_module |
| rustc_interface::passes::analysis | 죽은 코드 검사 | Map::par_for_each_module |
| rustc_interface::passes::analysis | 가시성 검사 | Map::par_for_each_module |
| rustc_lint::late::check_crate | 모듈별 린트를 실행합니다 | Map::par_for_each_module |
| rustc_typeck::check_crate | 적격성 검사 | Map::par_for_each_module |
병렬 반복자를 사용할 수 있는 루프가 아직 많이 남아 있습니다.
쿼리 시스템
쿼리 모델에는 큰 노력 없이도 여러 쿼리를 실제로 병렬 평가할 수 있게 해 주는 몇 가지 속성이 있습니다.
- 쿼리 프로바이더가 접근할 수 있는 모든 데이터는 쿼리 컨텍스트를 통하므로, 쿼리 컨텍스트가 접근 동기화를 처리할 수 있습니다.
- 쿼리 결과는 서로 다른 스레드에서 동시에 안전하게 사용할 수 있도록 불변이어야 합니다.
쿼리 foo를 평가할 때 foo의 캐시 테이블이 잠깁니다.
- 이미 결과가 있으면 그 결과를 복제하고 잠금을 해제하면 끝납니다.
- 캐시 항목이 없고 같은 결과를 계산하는 다른 활성 쿼리 호출도 없으면, 해당 키를 “진행 중“으로 표시하고 잠금을 해제한 뒤 평가를 시작합니다.
- 같은 키에 대한 다른 쿼리 호출이 진행 중이면 잠금을 해제하고, 다른 호출이 우리가 기다리는 결과를 계산할 때까지 스레드를 그저 블록합니다. 병렬 컴파일러에서의 순환 오류 탐지는 단일 스레드 모드보다 더 복잡한 로직을 필요로 합니다. 병렬 쿼리의 작업자 스레드들이 상호 의존성 때문에 더 이상 진행하지 못하면, 컴파일러는 추가 스레드(교착 상태 처리기라고 부름)를 사용해 순환 오류를 감지하고 제거하며 보고합니다.
병렬 쿼리 기능에는 아직 구현할 부분이 남아 있으며, 그 대부분은 앞서 설명한 Data Structures 및 Parallel Iterators와 관련되어 있습니다. 이 기능에 대한 열린 추적 이슈를 참조하십시오.
Rustdoc
2022년 11월 기준으로, `rustdoc` 렌더링을 병렬화하려면 아직 완료해야 할 단계가 여러 개 남아 있습니다([병렬 `rustdoc`][parallel-rustdoc]에 관한 공개 논의를 참고하십시오).참고 자료
더 자세히 알아보는 데 활용할 수 있는 자료는 다음과 같습니다.