Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

병렬 컴파일

기준 시점: 2024년 11월 현재 병렬 프런트엔드가 크게 변경되고 있으므로, 이 페이지에는 오래된 정보가 상당수 포함되어 있습니다.

추적 이슈: https://github.com/rust-lang/rust/issues/113349

2024년 11월 기준으로, Rust 컴파일러의 대부분이 이제 병렬화되어 있습니다.
  • 코드 생성 부분은 기본적으로 동시에 실행됩니다. -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)
ReadGuardparking_lot::RwLockReadGuardstd::cell::Ref
MappedReadGuardparking_lot::MappedRwLockReadGuardstd::cell::Ref
WriteGuardparking_lot::RwLockWriteGuardstd::cell::RefMut
MappedWriteGuardparking_lot::MappedRwLockWriteGuardstd::cell::RefMut
LockGuardparking_lot::MutexGuardstd::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::analysismatch 문의 유효성을 검사합니다Map::par_body_owners
rustc_interface::passes::analysisMIR 빌림 검사Map::par_body_owners
rustc_typeck::check::typeck_item_bodies타입 검사Map::par_body_owners
rustc_interface::passes::hir_id_validator::check_cratehir의 유효성을 검사합니다Map::par_for_each_module
rustc_interface::passes::analysis루프 본문, 속성, naked 함수, 불안정 ABI, const 본문의 유효성을 검사합니다Map::par_for_each_module
rustc_interface::passes::analysisMIR의 활성 검사 및 내재적 검사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]에 관한 공개 논의를 참고하십시오).

참고 자료

더 자세히 알아보는 데 활용할 수 있는 자료는 다음과 같습니다.