pre-RFC: Automatic generic-to-dynamic conversion

Summary

  • Include pre-monomorphised compiled version for items that are generic over object-safe trait objects when building a library crate;
  • Introduce a way for compiler to introduce dynamic dispatch “under the hood”, without modifying source code from <T> to &dyn.
  • Introduce a hint like #[prefer_dyn] that tells this generic item should be preferrably used in a dynamic way, even if source code looks generic. There can also be a more aggressive opt_level=s optimization making compiler introducing dyndispatch even not when asked.

Motivation

I saw a comment that compains that there is too much generic and too little dynamic in the ecosystem, leading unavoidably increased compile times and maybe binary sizes.

But explicitly making library APIs based dynamic dispatch instead of generic seems like a stepping down from Rust ideas and revoking user’s capability to use it in a generic way.

Example

Library:

pub trait MyTrait { fn lol(&self); }

struct Q1; struct Q2; struct Q3;

impl MyTrait for Q1, Q2, Q3 {...};

#[please_prefer_dyn_here]
fn my_func<T:MyTrait>(x : T) {
    // do something heavyweight, that requires a lof of compilation efforts
    // and not worth inlining or duplicating across those used types,
    // yet not revoking users of the generic use entirely.
}
  • Generic version serialized and included in rlib, like usual;
  • Additionally, a version as if it were fn my_func(x : &MyTrait) compiled and also included in rlib.
  • Library author can think again and just remove the attribute to easily revert to generic use without the artificial dyndispatch.
  • Attribute can be added in backward compatible way.

User:

my_func(Q1);
my_func(Q2);
#[please_prefer_generic_here]
my_func(Q3);
  • Looks like generic, but compiler uses dynamic dispatch under the hood, except for Q3 because of user explicitly requested it.
  • Compilation time of the user crate dropped because of no need to monomorphise, then compile generic 2 more times.

Drawbacks

  • More compile time when the feature is not used
  • More compilcations to the language, like usual

Alternatives

Providing helper functions like

fn foo_generic_version<T:Trait>(x: T);
fn foo(x: &Trait) {
    foo_generic_version(x)
}

so that dynamic dispatch becomes more popular without sacrificing static approach. Making some API Guidelines entry about that may also help.

TODO

Unresolved quesitons

  • Syntax of the hints

TODO

4 Likes

Why not the following syntax?

fn my_func<dyn T: MyTrait>(x: T) {...}

my_func::(Q2);
my_func::<!dyn _>(Q1);

At any rate, it probably needs to be something that is specified for each generic parameter rather than for the whole function.

1 Like

Exact syntax is details to be filled in. The main thing that it should be “out of band” optimisation hints that should not affect code semantics.

<dyn T> looks like as if it can only be used for dynamic dispatch, not compatible with usual <T>. But otherwise may be a good idea.

I’m sceptical that it would do a lot to drop compile times.

Items like Vec which are used many times in many ways are going to give quite significant overhead if replaced with a dynamic/run-time-generic version. On the other hand, quite often domain-specific generic code only gets used with a single parametrisation anyway.

Further, with the approach you suggest the compiler still has to create generic parametrisable forms of types (like my_func) when parsing the code because it doesn’t know that only the dynamic version will get used.

1 Like

I so want this.

Compile times aside (while it could decrease times of client code, it would also increase them for the generic code itself), this would be most beneficial when combined with dynamic linking, to reduce memory footprint. For the initial experiment, it isn’t even necessary to include any syntax/attributes, but rather just add a compiler argument that allows this (on both sides).

1 Like

As a further argument for not bothering with syntax at the beginning, it would technically count as a simple optimization, and you could implement and test it on all existing code without even needing RFC.

1 Like

Come to think of it, I’ve written some very large functions that take some <P: AsRef<Path>> or <R: Read> which are entirely finished using said object by the end of the first statement. Maybe we can solve that problem instead.

I.e. use some heuristic to transform this:

pub fn read<R: Read>(r: R) -> Result<Config> {
    let raw = ::serde_yaml::from_reader(r)?;

    // ...lots of code...
}

into this:

pub fn read<R: Read>(r: R) -> Result<Config> {
    // this gets compiled once
    fn _impl(a: ::serde_yaml::Result<RawConfig>) -> Result<Config> {
        // ...lots of code...
    }
    // this gets compiled many times
    _impl(::serde_yaml::from_reader(r))
}
12 Likes

Oh that’s slick! Thanks for the tip!

Sounds like an ideal use case for MIR optimizations, I think.

LLVM can do a more general form of this transformation under the name “outlining,” but that means doing the monomorphization and recognizing the common parts of the function body. MIR would be able to tell right away whether something depends on type parameters.

6 Likes

Related link: clap’s de-bloating.

A part of the solution was using trait objects and stepping down from generic to &dyn TraitObj.

1 Like

Related discussion: Reducing generics bloat

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.