-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Instantiate fewer copies of a closure inside a generic function #46477
Comments
cc @michaelwoerister @eddyb @arielb1 Seems like this has potential for some fairly large wins across Rust. |
This is a subset of being able to detect parameter dependence from MIR, and sharing instances on the monomorphization collector based on it. EDIT: in fact, I think all you need is to implement |
Interesting find! |
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. |
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? |
Assigning this to myself, going to be working on this optimisation as my master’s thesis. @rustbot claim |
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 |
How would this work, replace 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 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 Actually, there is probably a trick we can use: we can have the same generics as if |
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 Your
Sure, but that was already an artificial example, just trying to show the scope of generics.
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 Now, that If 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 Anyway, the neat thing is that you get the 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 But the fully generic form is this (note the lack of any mention of 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 The good news is that you would "just" need the analysis that <I as Iterator>::Item == <Map<Range<i32>, iter::{closure#0}> as Iterator>::Item which normalizes to (note that the type after <I as Iterator>::Item == i32 |
For those following along at home, there's a PR up for my work so far - #69749. |
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
f
and 2 copies ofg
, but unexpectedly 2 copies ofg::{{closure}}
in the IR.@Mark-Simulacrum
The text was updated successfully, but these errors were encountered: