RustMiddleTechnical
Что такое monomorphization и как это связано с generics в Rust?
Мономорфизация — компилятор генерирует отдельную версию обобщённой функции для каждого конкретного типа. Это даёт производительность без оверхеда, но увеличивает размер бинарника и время компиляции.
Monomorphization в Rust
Мономорфизация — это процесс, при котором компилятор Rust создаёт отдельную версию обобщённой функции или структуры для каждого конкретного типа, с которым она используется. Это происходит во время компиляции, а не в рантайме. В результате обобщённый код исполняется так же быстро, как ручной специализированный код.
Как это работает с generics
Когда вы пишете fn foo и вызываете её с i32 и String, компилятор создаёт две функции: foo_i32 и foo_String. В итоговом бинарнике нет никакого «универсального» кода — только специализированные версии.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers)); // версия для i32
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest char: {}", largest(&chars)); // версия для char
// Компилятор сгенерировал две отдельные функции
}
Мономорфизация структур
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
}
fn main() {
let mut int_stack: Stack<i32> = Stack::new();
int_stack.push(1);
let mut str_stack: Stack<String> = Stack::new();
str_stack.push("hello".to_string());
// Stack<i32> и Stack<String> — два разных типа в бинарнике
}
Связь с impl Trait и dyn Trait
impl Traitв позиции аргумента — синтаксический сахар для generic-параметра, мономорфизируется.impl Traitв позиции возврата — непрозрачный тип, компилятор знает конкретный тип, мономорфизируется.dyn Trait— динамическая диспетчеризация через vtable, мономорфизации нет.
fn static_dispatch(x: impl std::fmt::Display) {
println!("{}", x); // мономорфизируется
}
fn dynamic_dispatch(x: &dyn std::fmt::Display) {
println!("{}", x); // vtable, нет мономорфизации
}
Проверка в asm: cargo-asm
cargo install cargo-asm
cargo asm --release my_crate::largest
# Покажет специализированный asm для конкретного типа
Подводные камни
- Code bloat (раздутие бинарника): каждая уникальная комбинация типов генерирует свою копию кода. Большое количество generic-вызовов с разными типами увеличивает размер бинарника и время компиляции.
- Длинные времена компиляции: мономорфизация — дорогостоящий этап компиляции. Глубокие цепочки обобщённых функций существенно замедляют сборку.
- Смешение impl Trait и dyn Trait: рефакторинг
impl Traitвdyn Traitменяет производительность незаметно — прямые вызовы превращаются в indirect через vtable. - Специализация нестабильна:
#![feature(specialization)]позволяет переопределять реализации для конкретных типов, но функция нестабильна и её поведение может измениться. - Phantom type параметры: лишние phantom-параметры в структурах (
PhantomData) могут умножать количество мономорфизированных версий без реальной пользы. - Невидимый bloat в библиотеках: пользователи библиотеки не всегда осознают, что их разнообразные типы-аргументы генерируют копии кода из зависимости.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.