The compiler can do some optimizations like this, yes, but there's a lot more work to do in this area.
As an example of something folks still do by hand sometimes, take the starts_with method on Path: https://doc.rust-lang.org/stable/std/path/struct.Path.html#m...
It looks like this:
pub fn starts_with<P: AsRef<Path>>(&self, base: P) -> bool {
self._starts_with(base.as_ref())
}
fn _starts_with(&self, base: &Path) -> bool {
iter_after(self.components(), base.components()).is_some()
}
Why split this into two functions? Well, you can get smaller code size this way, because you're sort of "hand-de-duplicating" the parts that aren't generic. Once we call .as_ref, everything else is actually identical, but the compiler isn't good enough at this yet to do this itself, so we do it by hand.I wouldn't say this technique is super super common or well-known, just for the standard library (because it's used everywhere) and for folks that are sensitive to code size, like embedded or wasm people.
It is true that dyn means you don't get monomorphization, and can help with binary sizes.
EDIT: thinking about this some more, I wanted to say that it does feel similar, but one big difference that’s easy to explain is that dyn will change the way the value is represented in memory, and this will not. Conceptually, both do “cast and then call this single function”, but in the dyn case, the cast would be to a trait object, whereas this casts directly to &Path.
In the next part of the article I'll hopefully explore some drawbacks of choosing generics all the way down. Either driver would've likely perform a little better if developed fully independently, but by a smaller margin that I would've expected.
For example, see how switching from a concrete to a generic type increases the size of clone() by 4x: https://rust.godbolt.org/z/qbYr3v
This is because at the point the compiler synthesizes clone() it has less information about the type.
It's a bit of a chicken-and-egg problem. To monomorphize clone(), someone must emit an implementation first. But an optimal implementation requires analyses that aren't available until later in the pipeline. Here the optimization kicks in for types deriving Copy, but a generic parameter is enough to defeat it.
I'd be interested in an example where LLVM can't optimize the general version, as it means we might want to do this through MIR shims instead (which can be generated when collecting the monomorphic instances to codegen - this is what happens when you clone a tuple or closure, for example).
The link you provide shows only one function, because LLVM has optimized both to be identical, and deduplicated them.
(If you disable the "Directives" filter, you can see a `.set example::clone_concrete, example::clone_abstract`, which aliases one to the other)
The output with 1.45.2 is as follows:
example::clone_concrete:
mov eax, edi
ret
example::clone_abstract:
mov ecx, edi
and ecx, -256
xor eax, eax
xor edx, edx
cmp dil, 1
sete dl
cmove eax, ecx
or eax, edx
ret