단형화
아시다시피 Rust는 제네릭 타입을 폭넓게 지원하는 매우 표현력 있는 타입 시스템을 갖추고 있습니다. 하지만 당연히 어셈블리는 제네릭이 아니므로, 코드를 실행하기 전에 모든 제네릭의 구체 타입을 알아내야 합니다.
언어마다 이 문제를 다루는 방식이 다릅니다. 예를 들어 Java와 같은 일부 언어에서는 런타임이 되기 전까지 값의 가장 정확한 타입을 알지 못할 수도 있습니다. Java의 경우에는 (거의) 모든 변수가 어차피 참조 값(즉, 힙에 할당된 객체를 가리키는 포인터)이므로 이것이 문제가 되지 않습니다. 이러한 유연성은 성능을 희생한 대가입니다. 객체에 접근할 때마다 포인터를 역참조해야 하기 때문입니다.
Rust는 다른 접근 방식을 취합니다. Rust는 모든 제네릭 타입을 단형화합니다. 이는 컴파일러가 필요한 구체 타입마다 제네릭 함수 코드의 별도 사본을 찍어 낸다는 뜻입니다. 예를 들어 제 코드에서 Vec<u64>와 Vec<String>을 사용한다면, 생성된 바이너리에는 Vec에 대해 생성된 코드의 사본이 두 개 들어갑니다. 하나는 Vec<u64>용이고 다른 하나는 Vec<String>용입니다. 그 결과 프로그램은 빨라지지만, 컴파일 시간(그 모든 사본을 만드는 데 시간이 걸릴 수 있습니다)과 바이너리 크기(그 모든 사본이 많은 공간을 차지할 수 있습니다)라는 대가를 치르게 됩니다.
단형화는 Rust 컴파일러 백엔드의 첫 번째 단계입니다.
수집
먼저 프로그램에 있는 모든 제네릭 요소에 대해 어떤 구체 타입이 필요한지 알아내야 합니다. 이를 수집이라고 하며, 이 작업을 수행하는 코드를 단형화 수집기라고 합니다.
다음 예제를 보십시오.
fn banana() {
peach::<u64>();
}
fn main() {
banana();
}
단형화 수집기는 [main, banana, peach::<u64>]라는 목록을 제공합니다. 이들은 기계어 코드가 생성될 함수입니다. 수집기는 정적 항목(static) 같은 것들도 이 목록에 추가합니다.
자세한 내용은 수집기 rustdocs를 참고하십시오.
단형화 수집기는 MIR 로워링과 코드 생성 직전에 실행됩니다. rustc_codegen_ssa::base::codegen_crate는 collect_and_partition_mono_items 쿼리를 호출하며, 이 쿼리는 단형화 수집을 수행한 뒤 그 결과를 코드 생성 단위로 분할합니다.
코드 생성 단위(CGU) 분할
증분 빌드 시간을 개선하기 위해 CGU 분할기는 소스 수준 모듈마다 CGU를 두 개씩 만듭니다. 하나는 “안정적인” 코드, 즉 제네릭이 아닌 코드를 위한 것이고, 다른 하나는 더 자주 바뀌는 코드, 즉 단형화/특수화된 인스턴스를 위한 것입니다.
의존성의 경우, 크레이트 B가 크레이트 A에 의존하는 크레이트 A와 크레이트 B를 생각해 보십시오. 다음 표는 크레이트 A에 있는 함수가 크레이트 B의 모듈 하나 이상에서 사용될 수 있는 여러 시나리오를 보여 줍니다.
| 크레이트 A 함수 | 동작 |
|---|---|
| 비제네릭 함수 | 크레이트 A 함수가 크레이트 B의 어떤 코드 생성 단위에도 나타나지 않습니다 |
제네릭이 아닌 #[inline] 함수 | 크레이트 A 함수가 크레이트 B의 단일 CGU 안에 나타나며, 인라이닝 이후 단계가 지나도 계속 존재합니다 |
| 제네릭 함수 | 인라이닝 여부와 관계없이 크레이트 A의 모든 단형화된(특수화된) 함수 는 크레이트 B를 위한 단일 코드 생성 단위 안에 나타납니다. 이 코드 생성 단위는 인라이닝 이후 단계가 지난 뒤에도 존재합니다. |
제네릭 #[inline] 함수 | - 위와 같음 - |
파티셔너에 대한 자세한 내용은 모듈 수준 문서를 읽어 보십시오.