백엔드 독립적인 코드 생성
rustc_codegen_ssa는 모든 백엔드, 즉 LLVM, Cranelift, GCC가 구현할 추상 인터페이스를 제공합니다.
아래는 이 추상 인터페이스를 만든 리팩터링에 관한 배경 정보입니다.
rustc_codegen_llvm의 리팩터링
Denis Merigoux 작성, 2018년 10월 23일
리팩터링 이전의 코드 상태
MIR을 LLVM IR로 컴파일하는 것과 관련된 모든 코드는 rustc_codegen_llvm 크레이트 안에 들어 있었습니다. 가장 중요한 요소들을 나누어 보면 다음과 같습니다.
back폴더(7,800줄)는 LLVM을 통해 여러 오브젝트 파일과 아카이브를 생성하는 메커니즘뿐만 아니라 병렬 코드 생성을 위한 통신 메커니즘도 구현합니다.debuginfo폴더(3,200줄)에는 디버그 정보를 LLVM으로 전달하는 모든 코드가 들어 있습니다.llvm폴더(2,200줄)는 C++ API를 사용하여 LLVM과 통신하는 데 필요한 FFI를 정의합니다.mir폴더(4,300줄)는 MIR에서 LLVM IR로의 실제 로워링을 구현합니다.base.rs파일(1,300줄)에는 몇 가지 헬퍼 함수뿐만 아니라 코드 생성을 시작하고 작업을 분배하는 고수준 코드도 들어 있습니다.builder.rs파일(1,200줄)에는 기본 블록 안에서 개별 LLVM IR 명령어를 생성하는 모든 함수가 들어 있습니다.common.rs(450줄)에는 다양한 헬퍼 함수와 LLVM 정적 값을 생성하는 모든 함수가 들어 있습니다.type_.rs(300줄)는 LLVM IR로의 타입 변환 대부분을 정의합니다.
이 리팩터링의 목표는 이 크레이트 안에서 LLVM에 특화된 코드와 다른 rustc 백엔드에서 재사용할 수 있는 코드를 분리하는 것입니다. 예를 들어, mir 폴더는 거의 전적으로 백엔드에 특화되어 있지만, 크레이트의 다른 부분에 크게 의존합니다. 코드 분리는 코드의 로직에도, 성능에도 영향을 주어서는 안 됩니다.
이러한 이유로, 분리 과정에는 결과 코드가 컴파일되도록 동시에 수행해야 하는 두 가지 변환이 포함됩니다.
- 함수 시그니처와 구조체 정의 안의 모든 LLVM 특화 타입을 제네릭으로 대체합니다.
- LLVM FFI를 호출하는 모든 함수를, 백엔드 독립적인 코드와 백엔드 사이의 인터페이스를 정의할 트레이트 집합 안에 캡슐화합니다.
LLVM 특화 코드는 rustc_codegen_llvm에 남겨 두고, 새로운 트레이트와 백엔드 독립적인 코드는 모두 rustc_codegen_ssa로 옮깁니다(이름은 @eddyb가 제안했습니다).
제네릭 타입과 구조체
@irinagpopa는 rustc_codegen_llvm의 타입들을 제네릭 Value 타입으로 매개변수화하는 작업을 시작했으며, 이 타입은 LLVM에서 참조 &'ll Value로 구현됩니다. 이 작업은 mir 폴더 안팎의 모든 구조체뿐만 아니라 LLVM의 BasicBlock 및 Type 타입으로도 확장되었습니다.
LLVM 코드 생성에서 가장 중요한 두 구조체는 CodegenCx와 Builder입니다. 이들은 여러 라이프타임 매개변수와 Value의 타입으로 매개변수화되어 있습니다.
struct CodegenCx<'ll, 'tcx> {
/* ... */
}
struct Builder<'a, 'll, 'tcx> {
cx: &'a CodegenCx<'ll, 'tcx>,
/* ... */
}
CodegenCx는 여러 함수를 포함할 수 있는 하나의 코드 생성 단위(codegen-unit)를 컴파일하는 데 사용되고, Builder는 하나의 기본 블록을 컴파일하기 위해 생성됩니다.
rustc_codegen_llvm의 코드는 여러 명시적 라이프타임 매개변수를 다루어야 하며, 각각은 다음에 해당합니다.
'tcx는 가장 긴 라이프타임으로, 프로그램의 정보를 담고 있는 원래의TyCtxt에 해당합니다.'a는CodegenCx또는 구조체 안의 다른 객체에 대한 수명이 짧은 참조입니다.'ll은Value나Type같은 LLVM 객체에 대한 참조의 라이프타임입니다.
코드에는 이미 라이프타임 매개변수가 많이 있지만, 코드를 제네릭하게 만드는 과정에서 조작 대상인 LLVM 객체의 특수한 성질(이 객체들은 extern 포인터입니다) 때문에만 빌림 검사기를 통과하던 상황들이 드러났습니다. 예를 들어 analyse.rs의 LocalAnalyser에 라이프타임 매개변수를 하나 더 추가해야 했고, 그 결과 다음과 같이 정의되었습니다:
struct LocalAnalyzer<'mir, 'a, 'tcx> {
/* ... */
}
그러나 가장 중요한 두 구조체인 CodegenCx와 Builder는 백엔드에 독립적인 코드에 정의되어 있지 않습니다. 실제로 이 구조체들의 내용은 백엔드에 크게 특화되어 있으므로, 백엔드의 컨텍스트를 위한 제네릭 필드로 좁은 자리만 허용하기보다는 정의 자체를 백엔드 구현자에게 맡기는 편이 더 합리적입니다.
트레이트와 인터페이스
CodegenCx와 Builder는 백엔드가 정의해야 하므로, 백엔드의 인터페이스를 정의하는 모든 트레이트를 구현하는 구조체가 됩니다. 이 트레이트들은 rustc_codegen_ssa/traits 폴더에 정의되어 있으며, 백엔드에 독립적인 모든 코드는 이 트레이트들로 매개변수화되어 있습니다. 예를 들어 base.rs의 함수 하나가 어떻게 매개변수화되는지 설명해 봅시다:
pub fn codegen_instance<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>>(
cx: &'a Bx::CodegenCx,
instance: Instance<'tcx>
) {
/* ... */
}
이 시그니처에는 앞서 설명한 두 라이프타임 매개변수와, Builder 구조체가 충족하는 인터페이스에 해당하는 BuilderMethods 트레이트를 충족하는 마스터 타입 Bx가 있습니다. BuilderMethods는 연관 타입 Bx::CodegenCx를 정의하며, 이 타입은 다시 CodegenCx 구조체가 구현하는 CodegenMethods 트레이트를 충족합니다.
트레이트 쪽의 예로, 다음은 traits/builder.rs에 있는 BuilderMethods 정의의 일부입니다:
pub trait BuilderMethods<'a, 'tcx>:
HasCodegen<'tcx>
+ DebugInfoBuilderMethods<'tcx>
+ ArgTypeMethods<'tcx>
+ AbiBuilderMethods<'tcx>
+ IntrinsicCallMethods<'tcx>
+ AsmBuilderMethods<'tcx>
{
fn new_block<'b>(
cx: &'a Self::CodegenCx,
llfn: Self::Function,
name: &'b str
) -> Self;
/* ... */
fn cond_br(
&mut self,
cond: Self::Value,
then_llbb: Self::BasicBlock,
else_llbb: Self::BasicBlock,
);
/* ... */
}
마지막으로, base.rs의 codegen_crate처럼 코드 생성을 주도하는 고수준 함수에는 ExtraBackendMethods 트레이트를 구현하는 마스터 구조체가 사용됩니다. LLVM의 경우 이는 비어 있는 LlvmCodegenBackend입니다. ExtraBackendMethods는 rustc_codegen_ssa/src/traits/backend.rs에 정의된 CodegenBackend를 구현하는 구조체와 같은 구조체가 구현해야 합니다.
트레이트화 과정에서 일부 함수는 지역 구조체의 메서드에서 CodegenCx나 Builder의 메서드로 변환되었고, 그에 따라 self 매개변수가 추가되었습니다. 실제로 LLVM은 API를 통해 호출될 때 접근할 수 있는 정보를 내부에 저장합니다. 이 정보는 이러한 메서드가 호출될 때 함께 전달되는 Rust 자료 구조에는 나타나지 않습니다. 그러나 rustc용 Rust 백엔드를 구현할 때는 이러한 메서드가 CodegenCx의 정보를 필요로 하므로 매개변수가 추가된 것입니다(이 매개변수는 트레이트의 LLVM 구현에서는 사용되지 않습니다).
리팩터링 후 코드의 상태
이 트레이트들은 LLVM의 API와 매우 유사한 API를 제공합니다. LLVM은 작업 방식이 매우 독특하므로 이것이 최선의 해법은 아닙니다. 다른 백엔드를 추가할 때는 더 많은 유연성을 제공하기 위해 트레이트 정의가 변경될 수 있습니다.
그러나 현재 백엔드에 독립적인 코드와 LLVM 전용 코드를 분리한 덕분에 기존 rustc_codegen_llvm의 상당 부분을 재사용할 수 있었습니다. 가장 중요한 요소들에 대해 백엔드 독립적(BA) 코드와 LLVM 코드 사이의 새로운 줄 수 분포는 다음과 같습니다:
back폴더: 3,800 (BA) 대 4,100 (LLVM);mir폴더: 4,400 (BA) 대 0 (LLVM);base.rs: 1,100 (BA) 대 250 (LLVM);builder.rs: 1,400 (BA) 대 0 (LLVM);common.rs: 350 (BA) 대 350 (LLVM);
debuginfo 폴더는 분리 과정에서 거의 손대지 않은 채 남아 있으며 LLVM 전용입니다. 이 폴더의 고수준 기능만 트레이트화되었습니다.
새 traits 폴더에는 트레이트 정의만을 위한 코드가 1500줄 있습니다. 전체적으로 27,000줄 규모였던 기존 rustc_codegen_llvm 코드는 18,500줄 규모의 새 rustc_codegen_llvm과 12,000줄 규모의 rustc_codegen_ssa로 분리되었습니다. 이 리팩터링으로 인해, 그렇지 않았다면 rustc의 여러 백엔드 사이에서 중복되어야 했을 약 10,000줄의 코드를 재사용할 수 있게 되었다고 말할 수 있습니다.
리팩터링된 rustc 백엔드는 테스트 스위트와 성능 벤치마크 어느 쪽에서도 회귀를 일으키지 않았습니다. 이는 이번 리팩터링이 컴파일 타임 매개변수화만 사용했고 트레이트 객체는 사용하지 않았다는 특성과 일치하는 결과입니다.