드롭 정교화
동적 드롭
Rust 레퍼런스에 따르면 다음과 같습니다.
초기화된 변수나 임시 값이 스코프를 벗어나면 그 소멸자가 실행됩니다. 즉, 드롭됩니다. 대입 역시 왼쪽 피연산자가 초기화되어 있다면 그 소멸자를 실행합니다. 변수가 부분적으로 초기화된 경우에는 초기화된 필드만 드롭됩니다.
MIR을 구성할 때 Drop과 DropAndReplace 종결자는 드롭이 일어날 수 있는 위치를 나타냅니다. 그러나 이 단계에서는 이러한 종결자가 있다고 해서 소멸자가 반드시 실행된다는 보장은 없습니다. 종결자에 도달하기 전에 드롭 대상이 초기화되지 않은 상태일 수 있기 때문입니다(보통은 그 대상에서 값이 이동되었기 때문입니다). 일반적으로 변수가 초기화되었는지 여부는 컴파일 시점에 알 수 없습니다.
#![allow(unused)]
fn main() {
let mut y = vec![];
{
let x = vec![1, 2, 3];
if std::process::id() % 2 == 0 {
y = x; // conditionally move `x` into `y`
}
} // `x` goes out of scope here. Should it be dropped?
}
이러한 경우에는 변수가 초기화되었는지 여부를 동적으로 추적해야 합니다. 관련 규칙은 RFC 320: Non-zeroing dynamic drops에 자세히 설명되어 있습니다.
드롭 의무
RFC에서 인용하면 다음과 같습니다.
지역 변수가 초기화되면 “드롭 의무” 집합이 생깁니다. 드롭 의무란 드롭되어야 하는 구조적 경로(예: 지역 변수
a또는 필드로 가는 경로b.f.y)의 집합입니다.구조체 타입
T인 지역 변수 x의 드롭 의무는T의 구조를 분석하여 계산합니다.T자체가Drop을 구현한다면x가 드롭 의무가 됩니다.T가Drop을 구현하지 않는다면 드롭 의무 집합은T의 각 필드가 가진 드롭 의무의 합집합입니다.
구조적 경로에서 값이 이동되면(그래서 초기화되지 않은 상태가 되면) 그 경로나 그 하위 경로(path.f, path.f.g.h 등)에 대한 모든 드롭 의무가 해제됩니다. Drop 구현이 있는 타입은 개별 필드에서 값을 이동하는 것을 허용하지 않으므로, 그러한 타입을 거쳐 초기화 여부를 추적할 필요가 없습니다.
지역 변수가 스코프를 벗어날 때(Drop), 또는 구조적 경로가 대입으로 덮어쓰일 때(DropAndReplace) 해당 변수나 경로에 대한 드롭 의무가 있는지 확인합니다. 그 시점까지 해당 의무가 해제되지 않았다면 연관된 Drop 구현이 호출됩니다. enum 타입의 경우에는 “활성” 배리언트에 해당하는 필드만 드롭하면 됩니다. 이러한 타입의 드롭 의무를 처리할 때는 먼저 판별값을 검사하여 활성 배리언트를 결정합니다. 활성 배리언트를 제외한 나머지 배리언트의 드롭 의무는 모두 무시됩니다.
다음은 이 규칙을 설명하는 데 도움이 되는 몇 가지 흥미로운 타입입니다.
#![allow(unused)]
fn main() {
struct NoDrop(u8); // No `Drop` impl. No fields with `Drop` impls.
struct NeedsDrop(Vec<u8>); // No `Drop` impl but has fields with `Drop` impls.
struct ThinVec(*const u8); // Custom `Drop` impl. Individual fields cannot be moved from.
impl Drop for ThinVec {
fn drop(&mut self) { /* ... */ }
}
enum MaybeDrop {
Yes(NeedsDrop),
No(NoDrop),
}
}
드롭 정교화
이 규칙을 구현하는 타당한 모델 하나는 함수 안에서 한 번이라도 사용되는 모든 구조적 경로마다 불리언 플래그(“드롭 플래그”)를 하나씩 두는 것입니다. 이 플래그는 해당 경로가 초기화될 때 설정되고, 그 경로에서 값이 이동될 때 해제됩니다. Drop이 발생하면 Drop의 대상과 연관된 모든 의무의 플래그를 검사하고, 여전히 적용 가능한 의무에 대해 연관된 Drop impl을 호출합니다.
새로 빌드된 MIR의 부정확한 Drop 및 DropAndReplace 종결자를 드롭 플래그가 있는 형태로 변환하는 이 과정을 드롭 정교화(drop elaboration)라고 합니다. MIR 문장으로 인해 변수가 초기화(또는 초기화 해제)되면, 드롭 정교화는 해당 변수의 드롭 플래그를 설정(또는 해제)하는 코드를 삽입합니다. 또한 Drop 종결자를 새로 삽입된 드롭 플래그를 검사하는 조건문으로 감쌉니다.
드롭 정교화는 DropAndReplace 종결자를 대상에 대한 Drop과 방금 드롭된 위치에 대한 쓰기로 분리하기도 합니다. 이는 위에서 논의한 내용과는 다소 관련이 없습니다.
이 작업이 완료되면 MIR의 Drop 종결자는 드롭된 위치의 타입에 대한 “드롭 글루(drop glue)” 또는 “드롭 심(drop shim)” 호출에 대응합니다. 어떤 타입의 드롭 글루는 해당 타입의 Drop impl을 (존재한다면) 호출한 다음, 해당 타입의 모든 필드에 대해 드롭 글루를 재귀적으로 호출합니다.
rustc의 드롭 정교화
위에서 설명한 방식은 필요 이상으로 비용이 많이 듭니다. 몇 가지 최적화를 생각해 볼 수 있습니다.
Drop의 대상인 경로(또는 그 대상을 접두사로 갖는 경로)만 드롭 플래그가 필요합니다.- 일부 변수는 드롭될 때 초기화되어 있음(또는 초기화되어 있지 않음)이 알려져 있습니다. 이러한 변수에는 드롭 플래그가 필요하지 않습니다.
- 경로 집합이 공유된 접두사를 통해서만 드롭되거나 이동된다면, 해당 경로들은 하나의 드롭 플래그를 공유할 수 있습니다.
이 중 일부가 rustc에 구현되어 있습니다.
컴파일러에서 드롭 정교화는 여러 모듈에 나뉘어 있습니다. 패스 자체는 여기에 정의되어 있지만, 주요 로직은 드롭 심을 빌드하는 데에도 사용되므로 다른 곳에 정의되어 있습니다.
드롭 정교화는 새로 빌드된 MIR의 각 Drop을 다음 네 가지 종류 중 하나로 지정합니다.
Static: 대상이 항상 초기화되어 있습니다.Dead: 대상이 항상 초기화되어 있지 않습니다.Conditional: 대상이 전체적으로 초기화되어 있거나 전체적으로 초기화되어 있지 않습니다. 부분적으로 초기화되어 있지는 않습니다.Open: 대상이 부분적으로 초기화되어 있을 수 있습니다.
이를 위해 MaybeInitializedPlaces와 MaybeUninitializedPlaces라는 한 쌍의 데이터 흐름 분석을 사용합니다. 어떤 위치가 한쪽에는 있고 다른 쪽에는 없다면, 대상의 초기화 여부는 컴파일 시점에 알려져 있습니다(Dead 또는 Static). 이 경우 드롭 정교화는 대상에 대한 플래그를 추가하지 않습니다. 단순히 Drop 종결자를 제거하거나(Dead) 유지합니다(Static).
Conditional 드롭의 경우, 변수 전체의 초기화 여부가 그 필드들의 초기화 여부와 같다는 것을 알고 있습니다. 따라서 해당 드롭의 대상에 대한 드롭 플래그를 생성하고 나면, 그 대상에 대한 드롭 글루를 호출해도 안전합니다.
Open 드롭
Open 드롭은 가장 복잡합니다. 하나의 Drop 종결자를 여러 개의 서로 다른 종결자로 나누어야 하기 때문입니다. 이때 대상의 필드 가운데 타입에 드롭 글루가 있는(Ty::needs_drop) 필드마다 종결자가 하나씩 만들어집니다. 대상 자체에 대한 드롭 글루는 호출할 수 없습니다. 그렇게 하려면 대상의 모든 필드가 초기화되어 있어야 하기 때문입니다. 사용자 정의 Drop impl이 있는 타입의 변수는 필드를 이동해 낼 수 없으므로 Open 드롭을 허용하지 않는다는 점을 기억하십시오.
이는 각 필드를 Dead, Static, Conditional, Open 중 하나로 재귀적으로 분류하는 방식으로 이루어집니다. 타입에 드롭 글루가 없는 필드는 자동으로 Dead이므로 재귀 과정에서 고려할 필요가 없습니다. 종류가 Open이 아닌 필드에 도달하면 앞에서 설명한 방식대로 처리합니다. 필드도 Open이면 재귀가 계속됩니다.
열거형의 Open 드롭을 처리하는 방식은 짚어 볼 만합니다. 드롭 정교화 과정에서 열거형의 각 변형은 하나의 필드처럼 취급되며, 이러한 “변형 필드” 중 어느 시점에든 하나만 초기화될 수 있다는 불변 조건이 적용됩니다. 일반적인 경우에는 어떤 배리언트가 활성 상태인지 알 수 없으므로, (판별값을 검사하는) 열거형의 드롭 글루를 호출하거나, 정교화된 Open 드롭의 일부로서 판별값을 직접 검사해야 합니다. 하지만 특정한 경우(예를 들어 match 갈래 안)에는 열거형의 어느 변형이 활성 상태인지 알 수 있습니다. 이 정보는 비활성 변형에 해당하는 모든 위치를 초기화되지 않은 것으로 표시하는 방식으로 MaybeInitializedPlaces 및 MaybeUninitializedPlaces 데이터 흐름 분석에 인코딩됩니다.
정리 경로
TODO: 드롭 정교화와 언와인딩에 대해 설명합니다.
여담: 드롭 정교화와 상수 평가
Rust에서 컴파일 타임에 평가될 수 있는 함수는 const 키워드로 명시적으로 표시해야 합니다. 여기에는 Drop 트레이트의 구현도 포함되며, 이러한 구현은 const일 수도 있고 아닐 수도 있습니다. 컴파일 타임 평가 대상이 될 수 있는 코드는 const 함수만 호출할 수 있으므로, 그러한 코드에서 const가 아닌 Drop 구현을 호출하는 것은 모두 금지되어야 합니다.
Drop impl 호출은 MIR에서 Drop 종결자로 인코딩됩니다. 하지만 앞에서 설명했듯이, 새로 빌드된 MIR의 Drop 종결자가 반드시 Drop::drop 호출로 이어지는 것은 아닙니다. 그 시점에 드롭 대상이 초기화되어 있지 않을 수 있습니다. 따라서 새로 빌드된 MIR에서 const가 아닌 Drop을 검사하면 잘못된 오류가 발생할 수 있습니다. 그 대신 드롭 정교화가 실행된 뒤에 이러한 검사를 수행합니다. 드롭 정교화는 Dead 드롭(대상이 초기화되지 않은 것으로 알려진 드롭)을 제거합니다.