Background
People believe that generic code slows down compilation[1][2]. There's a whole section in The Rust Performance Book about ways to avoid IR bloat caused by generic functions. As an extreme example, glam
generates all its public interfaces' implementation with macros to avoid any generics, partly because generics can increase compile time.
Proposal
I believe we can mitigate this problem by allowing users to create Explicit MOnomorphizations(EMOs) of generic items (functions, methods, structs, enums). By creating an EMO in an upstream crate, things such as MIR and machine code are generated and stored in that crate's compilation artifact, so the downstream crates can skip generating those if they're using that specific flavor of the generic item.
This idea is so simple that I thought it must have been brought up previously, but I did my googling and found nothing. Was it proposed under a different name? Or is it just silly and I'm missing something obvious?
Anyway, the syntax in my mind looks like this. We introduce a new keyword mono
(of course open to bikeshedding), and we can write:
pub fn generic_fn<T>(t: T) {}
mono generic_fn::<i32>;
And for methods:
pub struct MonoStruct;
impl MonoStruct {
pub fn generic_method<T>(self, t: T) {}
mono fn generic_method::<i32>;
}
Of course we can create a struct EMO:
pub struct GenericStruct<T>;
mono GenericStruct::<i32>;
Creating an EMO for a generic method of a generic struct is more verbose and should be unusual:
pub struct GenericStruct<T>;
impl<T> GenericStruct<T> {
pub fn generic_method<U>(self, u: U) {}
}
mono impl::<i32> GenericStruct<i32> {
mono fn generic_method::<f32>;
}
Note that EMOs have the same visibility as their generic origin. EMOs of a private item make no sense.
Implementation
I'm not familiar enough with the compiler to lay out all the implementation details. My high level understanding is that EMO can be implemented in the mono item collector. Every EMO item becomes a mono item graph root. Before creating a new mono graph node, we check if there's an EMO version. If so, the node is not created.
Cross crate EMO
We can allow creating in crate B an EMO of an item defined in crate A. This is useful because an app author may want to create a crate with all the EMOs the app needs, and then the app's supporting library crates can all depend on it to avoid wasting compilation time. This method works like C++ precompiled headers.
However, allowing it can make the above discussed EMO lookup process slower, because we have to check all dependent crates for possible EMO.
Interaction with other language features
Lifetime parameters
AFAIK, lifetime parameters doesn't matter after borrow checking, which happens before monomorphization. EMOs can ignore lifetime parameters. We can write:
pub fn generic_fn<'a, T>(t: &'a T) {}
mono generic_fn::<i32>;
Specialization
An EMO is not a specialization. Any more specific implementation wins over EMO.
Inlining
One downside of EMO is that it makes inlining less likely. To mitigate this, we can allow #[inline]
attributes on EMOs.
Prior art
C++ compilers support precompiled headers, which is the closest thing I know to EMO.
EDIT: As @InfernoDeity pointed out, C++ has explicit instantiation.