Does specialization of generic functions always duplicate the function body in generated code?


#1

Say I have a function that looks something like this:

use std::path::Path;

fn do_complicated_thing(path: &Path) -> io::Result<()> {
    // Does something really complicated
}

This function is nice, but if I wanted to provide an &str for path, I’d have to wrap it in Path::new(), which isn’t very pretty. It’s much nicer to be able to just provide a string literal in some cases, and I can do that if I change the function signature:

fn do_complicated_thing<P: AsRef<Path>>(path: &P) -> ...

Now this function is relatively large, not the kind I would expect to get inlined into any call site if it’s used more than once. My concern is this: for each P which implements AsRef<Path> with which I invoke the function, will the compiler duplicate the entire function body, or will it be smart enough to “outline” the parts of the function which are independent of the generic parameter, and only specialize the parts which aren’t?


#2

I will let someone else answer more authoritatively, but yes I think so. You are responsible for factoring out the part that can be shared:



#3

Hmm. I would think that this factoring out could be eventually accomplished as a compiler optimization, even on Rust’s end, independent of LLVM. For instance, if you had sufficient analysis in the frontend to factor out parts of a function independent of type parameters, you could hoist them out to be separate, private functions in the LLVM AST. Then ideally, LLVM would merge them back in through inlining passes where it was deemed beneficial, and keep them separate otherwise.

Though since I haven’t actually looked at the rustc's internals (and don’t know much about LLVM myself), I have no idea how difficult it would be to implement, and what performance cost (for compilation) emitting all those extra functions could have in the average case. And I would guess this is pretty low on the list of performance issues, since it’s only applicable to a few cases, and can be sidestepped with a little boilerplate whenever it becomes a problem.


#4

It’s also something to pay attention to for effects on compile time.

In brief:

If a library’s function has any type parameter or is marked #[inline], then it is compiled to machine code by the crate that uses it.
a. This means the library compiles faster, since it doesn’t compile the function itself
b. User crates that don’t use the function don’t compile it at all
c. User crates that use the function recompile it every time they themselves recompile.

So generic or inline items are good in the way that you only compile what you use, but bad in the way that instead of compiling just once in the dependency, they recompile every time the usage’s crate changes.