rustc의 메모리 관리
일반적으로 rustc는 메모리 관리 방식에 상당히 주의를 기울입니다. 컴파일러는 컴파일 전반에 걸쳐 아주 많은 자료 구조를 할당하며, 주의하지 않으면 이 작업에 많은 시간과 공간이 소요됩니다.
컴파일러가 이를 관리하는 주요 방법 중 하나는 아레나와 인터닝을 사용하는 것입니다.
아레나와 인터닝
컴파일 중에는 매우 많은 자료 구조가 생성되므로, 성능상의 이유로 이들을 전역 메모리 풀에서 할당합니다. 각 데이터 구조는 수명이 긴 아레나에서 한 번만 할당됩니다. 이를 아레나 할당이라고 합니다. 이 시스템은 메모리 할당 및 해제 횟수를 줄여 줍니다. 또한 타입의 동등성을 쉽게 비교할 수 있게 해 줍니다(타입에 대한 자세한 내용은 여기를 참고하십시오). 인터닝된 각 타입 X에 대해 X에 대한 PartialEq를 구현해 두었으므로 포인터만 비교하면 됩니다. CtxtInterners 타입에는 인터닝된 타입들의 여러 맵과 아레나 자체가 들어 있습니다.
예시: ty::TyKind
컴파일러에서 타입을 나타내는 ty::TyKind를 예로 들어 보겠습니다(자세한 내용은 여기에서 읽을 수 있습니다). 컴파일러는 타입을 생성할 때마다 버퍼에서 단순히 할당하지 않습니다. 대신 해당 타입이 이미 생성되었는지 확인합니다. 이미 생성되었다면 이전에 받았던 것과 같은 포인터를 그대로 받고, 그렇지 않다면 새 포인터를 만듭니다. 이 방식에서는 두 타입이 같은지 알고 싶을 때 포인터만 비교하면 되므로 효율적입니다. ty::TyKind는 절대 스택에 생성해서는 안 되며, 그렇게 하면 사용할 수 없게 됩니다. 이들은 항상 이 아레나에서 할당하고, 고유하도록 항상 인터닝합니다.
컴파일을 시작할 때 버퍼를 하나 만들고, 타입을 할당해야 할 때마다 이 메모리 버퍼의 일부를 사용합니다. 공간이 부족해지면 버퍼를 하나 더 가져옵니다. 이 버퍼의 라이프타임은 'tcx입니다. 타입들은 이 라이프타임에 묶여 있으므로, 컴파일이 끝나면 해당 버퍼와 관련된 모든 메모리가 해제되고 'tcx 참조는 유효하지 않게 됩니다.
타입 외에도 할당할 수 있는 아레나 할당 자료 구조가 여러 가지 더 있으며, 이들은 이 모듈에서 찾을 수 있습니다. 다음은 몇 가지 예시입니다:
GenericArgs는mk_args로 할당됩니다 – 이는 타입의 슬라이스를 인터닝하며, 제네릭 인자에 대입될 값을 지정하는 데 자주 사용됩니다(예:HashMap<i32, u32>는 슬라이스&'tcx [tcx.types.i32, tcx.types.u32]로 표현됩니다).TraitRef는 일반적으로 값으로 전달됩니다 – 트레이트 참조는i32: Display처럼 트레이트에 대한 참조와 그 트레이트의 여러 타입 매개변수(Self포함)로 구성됩니다(여기서 def-id는Display트레이트를 참조하고, args에는i32가 들어 있습니다).def-id는AdtDef and DefId절에서 정의되고 자세히 다뤄진다는 점에 유의하십시오.Predicate는 트레이트 시스템이 증명해야 하는 대상을 정의합니다(traits 모듈을 참고하십시오).
tcx와 그 라이프타임 사용 방식
타이핑 컨텍스트(tcx)는 컴파일러의 핵심 자료 구조입니다. 이는 온갖 종류의 쿼리를 수행하는 데 사용하는 컨텍스트입니다. struct TyCtxt는 이 공유 컨텍스트에 대한 참조를 정의합니다:
tcx: TyCtxt<'tcx>
// ----
// |
// arena lifetime
보시다시피 TyCtxt 타입은 라이프타임 매개변수를 받습니다. 'tcx와 같은 라이프타임이 붙은 참조를 보면, 그것이 아레나에 할당된 데이터(또는 어쨌든 아레나만큼 오래 살아 있는 데이터)를 가리킨다는 것을 알 수 있습니다.
라이프타임에 관한 참고 사항
Rust 컴파일러는 큰 자료 구조(예: 추상 구문 트리(AST), 고수준 중간 표현(HIR), 타입 시스템)를 많이 포함하는 상당히 큰 프로그램이므로, 불필요한 메모리 사용을 최소화하기 위해 아레나와 참조에 크게 의존합니다. 이는 사람들이 컴파일러에 연결하는 방식(즉, 드라이버)에서도 드러나는데, 더 Rust다운 “pull” 방식(Iterator 트레이트를 떠올려 보십시오) 대신 “push” 방식의 API(콜백)를 선호합니다.
컴파일러 전반에서 스레드 로컬 저장소와 인터닝을 많이 사용하여 중복을 줄이는 동시에, 곳곳에 퍼져 있는 많은 라이프타임으로 인한 여러 사용성 문제도 방지합니다. rustc_middle::ty::tls 모듈은 이러한 스레드 로컬에 접근하는 데 사용되지만, 이 모듈을 직접 다뤄야 할 일은 거의 없을 것입니다.