-
Notifications
You must be signed in to change notification settings - Fork 14k
replace box_new with lower-level intrinsics #148190
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
base: main
Are you sure you want to change the base?
Conversation
|
Some changes occurred to MIR optimizations cc @rust-lang/wg-mir-opt The Miri subtree was changed cc @rust-lang/miri Some changes occurred in cc @BoxyUwU Some changes occurred in match checking cc @Nadrieril |
|
r? @SparrowLii rustbot has assigned @SparrowLii. Use |
| // rvalue, | ||
| // "Unexpected CastKind::Transmute {ty_from:?} -> {ty:?}, which is not permitted in Analysis MIR", | ||
| // ), | ||
| // } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This obviously needs to be resolved before landing... what should we do here? A transmute cast is always well-typed (it is UB if the sizes mismatch), so... can we just do nothing? I don't know what the type checker inside borrow checker is about.^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MIR typeck exists to collect all the lifetime constraints for borrowck to check. It also acts as a little bit of a double-check that typechecking on HIR actually checked everything it was supposed to, in some sense it's kind of the "soundness critical typeck". Having this do nothing seems fine to me, there's nothing to really typeck here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to still visit the cast type to find any lifetimes in there, or so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW this "is not permitted in Analysis MIR" part in the error I am removing here is odd as this is not documented in the MIR syntax where we usually list such restrictions, and also not checked by the MIR validator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to still visit the cast type to find any lifetimes in there, or so?
I don't think so, that should be handled by the super_rvalue call at the top of this visit_rvalue fn
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we do need to check something here, right now nothing enforces that the lifetimes match for the various uses of T in init_box_via_move<T>(b: Box<MaybeUninit<T>>, x: T) -> Box<T>.
| // Make sure `StorageDead` gets emitted. | ||
| this.schedule_drop_storage_and_value(expr_span, this.local_scope(), ptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here I am completely guessing... well really for all the changes in this file I am guessing, but the drop/storage scope stuff I know even less about than the rest of this.
| block, | ||
| source_info, | ||
| Place::from(ptr), | ||
| // Needs to be a `Copy` so that `b` still gets dropped if `val` panics. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a Miri test to ensure the drop does indeed happen. That's the easiest way to check for memory leaks...
| // Nothing below can panic so we do not have to worry about deallocating `ptr`. | ||
| // SAFETY: we just allocated the box to store `x`. | ||
| unsafe { core::intrinsics::write_via_move(ptr, x) }; | ||
| // SAFETY: we just initialized `b`. | ||
| unsafe { mem::transmute(ptr) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried using init_box_via_move here instead and it makes things a bit slower in some secondary benchmarks. I have no idea why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean write_box_via_move? Is the "bit slower" acceptable to simplify the implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should new_in and new_uninit_in get the same kind of inlined implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean write_box_via_move?
Yeah. (I renamed that intrinsic since writing the comment.)
Is the "bit slower" acceptable to simplify the implementation?
It doesn't simplify it by much, does it? Also it clearly leaves write_box_via_move as a vec!-only hack which I like.
Should new_in and new_uninit_in get the same kind of inlined implementation?
That seems orthogonal to this PR. They didn't use box_new before and are not involved with vec! either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This MIR is apparently so different from the previous one that it doesn't even show a diff (and the filename changed since I had to use CleanupPostBorrowck as built contains user types which contain super fragile DefIds). I have no idea what this test is testing and there are no filecheck annotations so... 😨
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these changes fine? Who knows! At least the filecheck annotations in the test still pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a regression test for an ICE where GVN would cast with wrong types. We only check a cast, the rest is boilerplate from inlining.
| StorageDead(_10); | ||
| StorageDead(_8); | ||
| StorageDead(_4); | ||
| drop(_3) -> [return: bb10, unwind: bb15]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is the relevant drop... but the before drop-elaboration MIR makes this quite hard to say. No idea why that's what the test checks. I think after drop elaboration this is a lot more obvious as the drops of moved-out variables are gone.
This comment has been minimized.
This comment has been minimized.
|
This PR modifies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fully a duplicate of something already tested in nll/user-annotations/patterns.rs.
| // FIXME: What is happening?!?? | ||
| let _: Vec<&'static String> = vec![&String::new()]; | ||
| //~^ ERROR temporary value dropped while borrowed [E0716] | ||
|
|
||
| let (_, a): (Vec<&'static String>, _) = (vec![&String::new()], 44); | ||
| //~^ ERROR temporary value dropped while borrowed [E0716] | ||
|
|
||
| let (_a, b): (Vec<&'static String>, _) = (vec![&String::new()], 44); | ||
| //~^ ERROR temporary value dropped while borrowed [E0716] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no idea what is happening here -- somehow code like let _: Vec<&'static String> = vec![&String::new()]; now compiles. I guess something is wrong with how I am lowering init_box_via_move?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably related to the transmutes there. Is there some way to insert those in a way that the lifetime constraints still get enforced?
This comment has been minimized.
This comment has been minimized.
4e526d4 to
cb7642b
Compare
This comment has been minimized.
This comment has been minimized.
86e5a72 to
020bc3a
Compare
This comment has been minimized.
This comment has been minimized.
|
I can't think of any way to actually preserve these lifetimes while using transmutes... so we'll have to add more method calls to So here's another redesign of the @bors try |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
replace box_new with lower-level intrinsics
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
fyi I'm out of the office today and can't look until later this week. |
|
Some changes occurred in diagnostic error codes |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
I'm going to wait with fixing Clippy until we agree we even want this change. |
| $crate::boxed::box_uninit_array_into_vec_unsafe( | ||
| $crate::intrinsics::write_box_via_move($crate::boxed::Box::new_uninit(), [$($x),+]) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Has an immediately invoked closure been considered?
(Though having an added closure for each occurance of vec! might have problems on its own. Is there a way to mark the closure inline(always) to prevent extra function calls when using a debug build?)
| $crate::boxed::box_uninit_array_into_vec_unsafe( | |
| $crate::intrinsics::write_box_via_move($crate::boxed::Box::new_uninit(), [$($x),+]) | |
| ) | |
| (|b| unsafe{ | |
| $crate::boxed::box_uninit_array_into_vec_unsafe(b) | |
| })( | |
| $crate::intrinsics::write_box_via_move( | |
| $crate::boxed::Box::new_uninit(), [$($x),+] | |
| ) | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, that does not work because $x might contain statements like return or break that won't work correctly inside the closure.
EDIT: Oh wait, the $x isn't even inside the closure. Yeah that could work, but given how perf-sensitive this is I'd rather not add a closure here.
And yes, #[inline(always)] works on closures.
|
☔ The latest upstream changes (presumably #148691) made this pull request unmergeable. Please resolve the merge conflicts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to wait with fixing Clippy until we agree we even want this change.
Thanks @RalfJung for the thorough investigation, much further than what I'd have done.
I'm generally in favor of this change, in particular because it simplifies MIR language.
However, this also introduces a lot of accidental complexity for perf reasons. I saw you did your best to reduce this complexity, so I won't bother you much on this.
| // Nothing below can panic so we do not have to worry about deallocating `ptr`. | ||
| // SAFETY: we just allocated the box to store `x`. | ||
| unsafe { core::intrinsics::write_via_move(ptr, x) }; | ||
| // SAFETY: we just initialized `b`. | ||
| unsafe { mem::transmute(ptr) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean write_box_via_move? Is the "bit slower" acceptable to simplify the implementation?
| // Nothing below can panic so we do not have to worry about deallocating `ptr`. | ||
| // SAFETY: we just allocated the box to store `x`. | ||
| unsafe { core::intrinsics::write_via_move(ptr, x) }; | ||
| // SAFETY: we just initialized `b`. | ||
| unsafe { mem::transmute(ptr) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should new_in and new_uninit_in get the same kind of inlined implementation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a regression test for an ICE where GVN would cast with wrong types. We only check a cast, the rest is boilerplate from inlining.
| .into(), | ||
| destination: inner_ptr, | ||
| target: Some(success), | ||
| unwind: UnwindAction::Continue, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
box_uninit_as_mut_ptr cannot unwind, so UnwindAction::Unreachable should be even better.
requires lowering write_via_move during MIR building to make it just like an assignment
This allows us to get rid of box_new entirely
|
This PR was rebased onto a different master commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
This comment has been minimized.
This comment has been minimized.
|
Some changes occurred in src/tools/clippy cc @rust-lang/clippy |
|
@rust-lang/clippy I need help dealing with the remaining clippy issues here.
|
|
The job Click to see the possible cause of the failure (guessed by this bot) |
| with_new.unwrap_or_else(Vec::new); | ||
| //~^ unwrap_or_default |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a regression of the unwrap_or_default lint in Clippy.
(I only scrolled through the PR convos quickly. So if this was brought up earlier and resolved, ignore me)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@flip1995 please have a look at this comment :)
The
box_newintrinsic is super special: during THIR construction it turns into anExprKind::Box(formerly known as theboxkeyword), which then during MIR building turns into a special instruction sequence that invokes the exchange_malloc lang item (which has a name from a different time) and a special MIR statement to represent a shallowly-initializedBox(which raises interesting opsem questions).This PR is the n-th attempt to get rid of
box_new. That's non-trivial because it usually causes a perf regression: replacingbox_newby naive unsafe code will incur extra copies in debug builds, making the resulting binaries a lot slower, and will generate a lot more MIR, making compilation measurably slower. Furthermore,vec!is a macro, so the exact code it expands to is highly relevant for borrow checking, type inference, and temporary scopes.To avoid those problems, this PR does its best to make the MIR almost exactly the same as what it was before.
box_newis used in two places,Box::newandvec!:Box::newthat is fairly easy: themove_by_valueintrinsic is basically all we need. However, to avoid the extra copy that would usually be generated for the argument of a function call, we need to special-case this intrinsic during MIR building. That's what the first commit does.vec!is a lot more tricky. As a macro, its details leak to stable code, so almost every variant I tried broke either type inference or the lifetimes of temporaries in some ui test or ended up accepting unsound code due to the borrow checker not enforcing all the constraints I hoped it would enforce. I ended up with a variant that involves a new intrinsic,fn write_box_via_move<T>(b: Box<MaybeUninit<T>>, x: T) -> Box<MaybeUninit<T>>, that writes a value into aBox<MaybeUninit<T>>and returns that box again. The MIR building code for this is non-trivial, but in exchange we can get rid of somewhat similar code in the lowering forExprKind::Box. Sadly, we need a new lang item as I found no other way to get a pointer of the right type in MIR, but we do get rid of the exchange_malloc lang item in exchange. (We can also get rid ofRvalue::ShallowInitBox; I didn't include that in this PR -- I think @cjgillot has a commit for this somewhere.)See here for the latest perf numbers. Most of the regressions are in deep-vector which consists entirely of an invocation of
vec!, so any change to that macro affects this benchmark disproportionally.This is my first time even looking at MIR building code, so I am very low confidence in that part of the patch, in particular when it comes to scopes and drops and things like that.
vec!FAQwrite_box_via_movereturn theBoxagain? Because we need to expandvec!to a bunch of method invocations without any blocks or let-statements, or else the temporary scopes (and type inference) don't work out.box_uninit_array_into_vec_unsafe(unsoundly!) a safe function? Because we can't use an unsafe block invec!as that would necessarily also include the$x(due to it all being one big method invocation) and therefore interpret the user's code as being insideunsafe, which would be bad (and 10 years later, we still don't have safe blocks for macros like this).write_box_via_moveuseBoxas input/output type, and not, say, raw pointers? Because that is the only way to get the correct behavior when$xpanics or has control effects: we need theBoxto be dropped in that case. (As a nice side-effect this also makes the intrinsic safe, which is imported as explained in the previous bullet.)*mut Box<MaybeUninit<T>>to*mut T? Because we need to do that conversion when compilingwrite_box_via_moveto MIR, and we can't do that conversion in pure MIR. We can't transmute because then the borrow checker loses track of how the lifetimes flow through these calls, which would be unsound. We can't get the raw pointer out from inside theBoxvia field projections because that is a*constpointer which we can't write to. We can't cast that to*mutbecause then we again lose track of the lifetimes. We can't change that pointer to be*mutbecause thenNonNullwould have the wrong variance (and 10 years later, we still don't have variance annotations).write_box_via_movereturnBox<T>? Yes we could, but there's no easy way for the intrinsic to convert itsBox<MaybeUninit<T>>to aBox<T>. We would need another lang item.