Repository navigation
Instantiate fewer copies of a closure inside a generic function #46477
Description
Activity
cc @michaelwoerister @eddyb @arielb1
Seems like this has potential for some fairly large wins across Rust.
Reacted by Mazdak Farrokhzad, Eduard-Mihai Burtescu, Sam Nardoni, Wesley Wiser, Jon Gjengset, Martin Carton, Bennet Bleßmann and Jakub Beránek- addedI-compiletimeIssue: Problems and improvements with respect to compile times.Issue: Problems and improvements with respect to compile times.P-mediumMedium priorityMedium priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.
on Dec 3, 2017 This is a subset of being able to detect parameter dependence from MIR, and sharing instances on the monomorphization collector based on it.
Should be relatively straight-forward nowadays.EDIT: in fact, I think all you need is to implement
TypeVisitor::visit_tyand put MIR through it, accumulating a bitset of "does this type parameter appear", at least on the analysis side.Reacted by Taylor Cramer, Kornel, Andy Russell and Russell JohnstonInteresting find!
- addedC-enhancementCategory: An issue proposing an enhancement or a PR with one.Category: An issue proposing an enhancement or a PR with one.
on Dec 5, 2017 Just to leave a breadcrumb for later, there are other good suggestions for similar kinds of optimizations that can be done in this internals thread.
- addedWG-compiler-performanceWorking group: Compiler PerformanceWorking group: Compiler Performance
on May 9, 2018 This came up in conversation at a meetup recently. Several of us thought it would be interesting to see how big in impact it makes. None of us have any experience working on the compiler. How hard is this for a new contributor? Is there mentorship available? Alternatively does someone want to do some kind of remote presentation for our meetup, guiding us on this?
Reacted by Jon GjengsetAssigning this to myself, going to be working on this optimisation as my master’s thesis.
@rustbot claim
Reacted by Josh Stone, Russell Johnston, Eduard-Mihai Burtescu, Emil Lauridsen, Mateusz Mikuła, Rémy Rakic, Jake Goulding, Jonas Schievink, Jon Gjengset, varkor and 21 morein fact, I think all you need is to implement
TypeVisitor::visit_tyand put MIR through it, accumulating a bitset of "does this type parameter appear", at least on the analysis side.Would this see through associated types?
The question of type sizes came up again in the users forum, akin to #62429. I gave this example of how things can go bad, and how to manually fix it:
fn multiply<I>(iter: I, x: f64) -> impl Iterator<Item = f64> where I: Iterator, I::Item: Into<f64>, { iter.map(move |item| x * item.into()) } fn multiply2<I>(iter: I, x: f64) -> impl Iterator<Item = f64> where I: Iterator, I::Item: Into<f64>, { fn mul<T: Into<f64>>(x: f64) -> impl Fn(T) -> f64 { move |item| x * item.into() } iter.map(mul(x)) } fn iter() -> impl Iterator<Item = i32> { (0..10).map(|i| i * 42) } pub fn foo() { let _ = multiply(iter(), 2.0); let _ = multiply2(iter(), 2.0); }
This creates expanded types like this:
; playground::foo ; Function Attrs: nonlazybind uwtable define void @_ZN10playground3foo17hbce2de427f35bc00E() unnamed_addr #1 !dbg !161 { start: %_3 = alloca %"core::iter::adapters::Map<core::iter::adapters::Map<core::ops::range::Range<i32>, iter::{{closure}}>, multiply2::mul::{{closure}}<i32>>", align 8 %_1 = alloca %"core::iter::adapters::Map<core::iter::adapters::Map<core::ops::range::Range<i32>, iter::{{closure}}>, multiply::{{closure}}<core::iter::adapters::Map<core::ops::range::Range<i32>, iter::{{closure}}>>>", align 8 ...
It would be nice if
multiply::{{closure}}could automatically be reduced like I did manually formultiply2::mul::{{closure}}. It seems to me that "does this type parameter appear" would have to see through to the associated typeI::Item, and not count that as an appearance ofIitself.It seems to me that "does this type parameter appear" would have to see through to the associated type
I::Item, and not count that as an appearance ofIitself.How would this work, replace
I::Itemwith a generic parameter?
You can do it manually like this, but it seems harder for the compiler:fn multiply3( iter: impl Iterator<Item = impl Into<f64>>, x: f64, ) -> impl Iterator<Item = f64> { iter.map(move |item| x * item.into()) }
for the record, that is equivalent to:
fn multiply3<I, T>(iter: I, x: f64) -> impl Iterator<Item = f64> where I: Iterator<Item = T>, T: Into<f64>, { iter.map(move |item| x * item.into()) }
I think we need to wait for @davidtwco's work to be merged before we can even consider something like this.
You might also want to consider
iter.map(Into::into).map(move |y| x * y).Keep in mind that monomorphization happens based on generics, so you'd have to come up with some generics that still encapsulate the fact that there's a type which is needed by the
Into::intocall, and that's much harder when they're not the type-checking generics (for which you can simply not replace some params with their args).Actually, there is probably a trick we can use: we can have the same generics as if
Iwas unused, but then add a<I as Item>::Item == Xbound to theParamEnvfor everyXwe monomorphize on.
That way the compiler doesn't have to invent generics, and IMO that's also the way I would want to handle monomorphizing only based on the size/align of a type but nothing else.
(we'd have bounds in theParamEnvdescribing those properties)It seems to me that "does this type parameter appear" would have to see through to the associated type
I::Item, and not count that as an appearance ofIitself.How would this work, replace
I::Itemwith a generic parameter?Something like that, yes. (Internal only to the construction of the closure -- we wouldn't want to silently affect the user's API.) I'm sure it is a harder request for the compiler, but this issue was cited as a possible solution to replace #62429 -- in a lot of those cases, the whole point was to be generic on the
Itemtype rather than the broader iterator.Your
Item = impl ...trick is neat for my specific example, but I don't think that will always apply. Those real cases onIterators are dealing with parameters ofSelf(likeMap<I, F>) and then doing something in a closure withI::ItemorSelf::Item. We also can't change the API of those methods to add new type parameters, whether explicit orimpl ....You might also want to consider
iter.map(Into::into).map(move |y| x * y).Sure, but that was already an artificial example, just trying to show the scope of generics.
Actually, there is probably a trick we can use: we can have the same generics as if
Iwas unused, but then add a<I as Item>::Item == Xbound to theParamEnvfor everyXwe monomorphize on.
That way the compiler doesn't have to invent generics, and IMO that's also the way I would want to handle monomorphizing only based on the size/align of a type but nothing else.
(we'd have bounds in theParamEnvdescribing those properties)I don't know enough of these details, but it sounds plausible to me! :)
To expand a bit, the monomorphization is keyed today on:
fn multiply::<Map<Range<i32>, iter::{closure#0}>>::{closure#0}; fn multiply2::mul::<i32>::{closure#0}; fn multiply3::<Map<Range<i32>, iter::{closure#0}>, i32>::{closure#0};
with @davidtwco's work, it should look like this:
fn multiply::<Map<Range<i32>, iter::{closure#0}>>::{closure#0}; fn multiply2::mul::<i32>::{closure#0}; fn multiply3::<I, i32>::{closure#0} where I: Sized, I: Iterator, <I as Iterator>::Item == i32, i32: Sized, ;
(I'm using the version of
multiply3with a namedIjust to make things clearer)Now, that
whereclause I wrote there is theParamEnv, i.e. how the compiler tracks "bounds" in scope, and we might have it from the start because e.g.&mut Ionly has a known layout ifI: Sizedis known (makes more sense for e.g.Vec::lenI guess).If
Twould also be unused, you'd get the fully genericParamEnv, i.e.:fn multiply3::<I, T>::{closure#0} where I: Sized, I: Iterator, <I as Iterator>::Item == T, T: Sized, ;
And you can see there that the
T = i32version I had at first is literally the same except withi32instead ofT, effectively a "partial substitution".Anyway, the neat thing is that you get the
<I as Iterator>::Item == i32bound in scope "for free" withmultiply3+ @davidtwco's initial approach, meaning the MIR body of the closure could actually useI::Iteminstead ofTand it would still resolve asi32.So codegen wouldn't need to be changed in order to do this monomorphization:
fn multiply::<I>::{closure#0} where I: Sized, I: Iterator, <I as Iterator>::Item == i32, ;
As you can see, it's literally
multiply3minus the second type parameter and the redundant-after-substitutioni32: Sizedbound.But the fully generic form is this (note the lack of any mention of
I::Item):fn multiply::<I>::{closure#0} where I: Sized, I: Iterator, ;
So you have to come up with that extra bound and inject it into the
ParamEnv.The good news is that you would "just" need the analysis that
Iisn't used, only<I as Iterator>::Itemis (which isn't that hard,ty::layoutalso has some special-casing for "type parameter or associated type projection"), and then generate this bound:<I as Iterator>::Item == <Map<Range<i32>, iter::{closure#0}> as Iterator>::Item
which normalizes to (note that the type after
==has no generics):<I as Iterator>::Item == i32
Reacted by Josh Stone and Sergei ShulepovFor those following along at home, there's a PR up for my work so far - #69749.
Reacted by Jonas Platte, Eduard-Mihai Burtescu, Rémy Rakic, Jake Goulding, Paa Kojo Samanpa, Jon Gjengset, Josh Stone, Kornelijus, memoryruins, runiq and 10 moreReacted by mark, Tony Arcieri, Nicolas Abram and Innokentii Meleshchenko
In serde-rs/json#386 we observed that a disproportionately large amount of serde_json lines of LLVM IR and compile time are due to a tiny closure inside a generic function. In fact this closure contributes more LLVM IR than all but 5 significantly larger functions.
The generic function needs to be instantiated lots of times, but the closure does not capture anything that would be affected by the type parameter.
Simplified example:
This gives the expected 1 copy of
fand 2 copies ofg, but unexpectedly 2 copies ofg::{{closure}}in the IR.@Mark-Simulacrum