일반화된 유형(제네릭)은 특정 유형에 독립적인 코드를 작성할 수 있게 해줍니다. 이는 각괄호 구문을 통해 구현됩니다:
fn max<T: PartialOrd>(a: T, b: T) -> T { if a > b { a } else { b } }
여기서 T는 PartialOrd 트레이트로 제약된 일반화된 유형입니다.
제네릭 매개변수는 <T>를 통해 선언되지만 트레이트 경계를 사용하여 제한할 수 있습니다, 예를 들면 <T: Display>와 같이. 이는 컴파일러에게 필요한 트레이트가 구현된 유형만 사용할 수 있음을 알리는 방법입니다.
Rust에서는 제네릭에 대해 두 가지 형태의 배치(dispatch)가 있습니다:
dyn Trait가 사용되는 경우 가상 테이블(vtable)을 통해 호출이 발생합니다.기계 코드에 미치는 영향: 트레이트 경계가 있는 제네릭 사용( dyn Trait 없이)은 모노모프화를 초래하여 바이너리 크기가 증가하지만 최대 속도를 보장합니다. dyn Trait 사용은 바이너리 크기를 줄이지만 성능 저하가 발생합니다.
질문:
fn do_something<T: Debug>(value: &T)
컴파일러가 이 함수의 각 유형에 대해 이진 코드에서 별도의 do_something 함수를 생성할 것인지, 아니면 일반 구현을 사용할 것인가요?
일반적인 잘못된 답변: 트레이트 경계 덕분에 모든 유형에 대해 하나의 함수가 사용됩니다.
올바른 답변: 컴파일러는 각 유형에 대해 이 함수의 별도의 복사본을 생성합니다(모노모프화). 트레이트 경계는 제네릭 함수를 vtable을 통한 "일반적인" 것으로 만들지 않습니다. 일반성은 dyn Trait (동적 배치)에서만 나타납니다.
예제:
fn print_val<T: std::fmt::Debug>(val: T) { println!("{:?}", val); } // 각기 다른 유형으로 호출될 때마다 고유한 버전의 함수가 생성됩니다.
이야기
큰 제네릭 객체가 있는 프로젝트에서 바이너리 파일 크기가 예상보다 훨씬 커졌다는 것이 발견되었습니다. 나중에, 그 원인은 제한 없이 일반화된 함수의 광범위한 사용에서 비롯된 것으로 밝혀졌습니다. 수십 가지 유형으로의 호출이 실행 파일의 크기(코드 팽창)를 기하급수적으로 증가시켰고 이는 CI에서 릴리스 빌드에서만 확인되었습니다.
이야기
한 개발자가 트레이트 경계를 가진 제네릭 매개변수를 받아들이며, 이러한 코드가 "동적" 배치와 함께 작동한다고 생각했습니다. 이는 서버의 메모리 사용 증가와 코드와 프로세서 캐시 증가로 인한 성능 저하로 이어졌습니다.
이야기
라이브러리에서
Self유형(예:trait Clone)을dyn Trait으로 사용하려고 했으나 Rust가 지원하지 않아 컴파일 오류가 발생했습니다. 인터페이스를 명시적으로 다시 작성해야 했고, 그렇지 않으면 제네릭 API가 동적 모드에서 작동하지 않았으며, 인터페이스를 컴파일 타임 수준에서 변경해야 했습니다.