Skip to content

Conversation

@RalfJung
Copy link
Member

The box_new intrinsic is super special: during THIR construction it turns into an ExprKind::Box (formerly known as the box keyword), 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-initialized Box (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: replacing box_new by 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.

To avoid those problems, this PR does its best to make the MIR almost exactly the same as what it was before. box_new is used in two places, Box::new and vec!:

  • For Box::new that is fairly easy: the move_by_value intrinsic 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. I ended up with a variant that involves a new intrinsic, init_box_via_move, that write a value into a Box<MaybeUninit<T>> and returns that box as Box<T>. The MIR building code for this is non-trivial, but in exchange we can get rid of somewhat similar code in the lowering for ExprKind::Box. (We can also get rid of Rvalue::ShallowInitBox; I didn't include that in this PR -- I think @cjgillot has a commit for this somewhere.)

I ran lots of perf experiments for this in #147907. The final version is this. 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.

requires lowering write_via_move during MIR building to make it just like an assignment
@rustbot
Copy link
Collaborator

rustbot commented Oct 27, 2025

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 rustc_ty_utils::consts.rs

cc @BoxyUwU

Some changes occurred in match checking

cc @Nadrieril

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Oct 27, 2025
@rustbot
Copy link
Collaborator

rustbot commented Oct 27, 2025

r? @SparrowLii

rustbot has assigned @SparrowLii.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

// rvalue,
// "Unexpected CastKind::Transmute {ty_from:?} -> {ty:?}, which is not permitted in Analysis MIR",
// ),
// }
Copy link
Member Author

@RalfJung RalfJung Oct 27, 2025

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.^^

Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member Author

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>.

Comment on lines 445 to 449
// Make sure `StorageDead` gets emitted.
this.schedule_drop_storage_and_value(expr_span, this.local_scope(), ptr);
Copy link
Member Author

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.
Copy link
Member Author

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...

Comment on lines +292 to +307
// 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) }
Copy link
Member Author

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.

Copy link
Member Author

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... 😨

Copy link
Member Author

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.

StorageDead(_10);
StorageDead(_8);
StorageDead(_4);
drop(_3) -> [return: bb10, unwind: bb15];
Copy link
Member Author

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.

@rust-log-analyzer

This comment has been minimized.

@rustbot
Copy link
Collaborator

rustbot commented Oct 28, 2025

This PR modifies tests/ui/issues/. If this PR is adding new tests to tests/ui/issues/,
please refrain from doing so, and instead add it to more descriptive subdirectories.

Copy link
Member Author

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.

Comment on lines 63 to 69
// 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]
}
Copy link
Member Author

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?

Copy link
Member Author

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?

@rust-log-analyzer

This comment has been minimized.

@RalfJung RalfJung force-pushed the box_new branch 2 times, most recently from 4e526d4 to cb7642b Compare October 28, 2025 07:41
@rust-log-analyzer

This comment has been minimized.

@RalfJung RalfJung force-pushed the box_new branch 2 times, most recently from 86e5a72 to 020bc3a Compare October 28, 2025 09:33
@rust-log-analyzer

This comment has been minimized.

@RalfJung
Copy link
Member Author

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 vec!, which will show up in perf. On the plus side it seems I misunderstood the errors I saw before regarding temporary scopes around vec!... or may our test suite just can't reproduce those problems.

So here's another redesign of the vec! macro. Macros were a mistake, and this one in particular has turned into my worst nightmare...

@bors try
@rust-timer queue

@rust-timer
Copy link
Collaborator

Awaiting bors try build completion.

@rustbot label: +S-waiting-on-perf

@rust-bors

This comment has been minimized.

rust-bors bot added a commit that referenced this pull request Oct 28, 2025
replace box_new with lower-level intrinsics
@rustbot rustbot added the S-waiting-on-perf Status: Waiting on a perf run to be completed. label Oct 28, 2025
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not getting a suggestion any more here. No idea why, but I don't think it's worth worrying about.

@RalfJung
Copy link
Member Author

RalfJung commented Oct 28, 2025

Actually never mind, we do have an rvalue scope test for vec! and it breaks again. (EDIT: Oh, and type inference also breaks. How great. The code should be entirely equivalent to what we had before in terms of type inference degrees of freedom, but apparently our type inference has some fun non-predictable extra side-effects for extra fun.)

fn one() -> i32 { 1 }

// Make sure the vec![...] macro doesn't introduce hidden rvalue
// scopes (such as blocks) around the element expressions.
pub fn main() {
    assert_eq!(vec![&one(), &one(), &2], vec![&1, &1, &(one()+one())]);
    assert_eq!(vec![&one(); 2], vec![&1, &one()]);
}

So, we definitely need a special "initialize that box" intrinsic. It's entirely impossible to get the same behavior with more reasonable, smaller-scoped operations without introducing extra copies or extra blocks/scopes and while ensuring the uninitialized box gets dropped if the expression panics (or returns).

The only option I can still think of is to have the intrinsic generate a method call to convert &mut Box<MaybeUninit<T>> into *mut T. That would need a new lang item. Or maybe a new kind of primitive cast or so. Anything that ensures there's a lifetime relationship between the two uses of T.

@rust-log-analyzer

This comment was marked as outdated.

@rust-log-analyzer

This comment was marked as outdated.

@rust-bors

This comment was marked as outdated.

@rustbot
Copy link
Collaborator

rustbot commented Oct 28, 2025

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

Some changes occurred to constck

cc @fee1-dead

@RalfJung
Copy link
Member Author

@bors try

@rust-bors
Copy link

rust-bors bot commented Oct 28, 2025

⌛ Trying commit 575ae70 with merge 651b5b1

To cancel the try build, run the command @bors try cancel.

Workflow: https://github.com/rust-lang/rust/actions/runs/18876036154

rust-bors bot added a commit that referenced this pull request Oct 28, 2025
replace box_new with lower-level intrinsics
@rust-log-analyzer

This comment has been minimized.

This allows us to get rid of box_new entirely
@rustbot
Copy link
Collaborator

rustbot commented Oct 28, 2025

Some changes occurred in compiler/rustc_codegen_cranelift

cc @bjorn3

Some changes occurred in compiler/rustc_codegen_gcc

cc @antoyo, @GuillaumeGomez

@rust-log-analyzer
Copy link
Collaborator

The job aarch64-gnu-llvm-20-1 failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)

---- [debuginfo-gdb] tests/debuginfo/macro-stepping.rs stdout ----
NOTE: compiletest thinks it is using GDB version 16003000

error: check directive(s) from `/checkout/tests/debuginfo/macro-stepping.rs` not found in debugger output. errors:
    (macro-stepping.rs:35) `[...]#loc6[...]`
the following subset of check directive(s) was found successfully:
    (macro-stepping.rs:20) `110     foo!(); // #loc1`
    (macro-stepping.rs:23) `112     foo2!(); // #loc2`
    (macro-stepping.rs:26) `114     let x = vec![42]; // #loc3`
    (macro-stepping.rs:29) `116     new_scope!(); // #loc4`
    (macro-stepping.rs:32) `118     println!("Hello {}", // #loc5`
    (macro-stepping.rs:40) `2     foo!(); // #inc-loc1`
    (macro-stepping.rs:43) `4     foo2!(); // #inc-loc2`
    (macro-stepping.rs:46) `6     zzz(); // #inc-loc3`
status: exit status: 0
command: PYTHONPATH="/checkout/src/etc" "/usr/bin/gdb" "-quiet" "-batch" "-nx" "-command=/checkout/obj/build/aarch64-unknown-linux-gnu/test/debuginfo/macro-stepping.gdb/macro-stepping.debugger.script"
--- stdout -------------------------------
GNU gdb (Ubuntu 16.3-1ubuntu2) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "aarch64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
Breakpoint 1 at 0x11b8: file /checkout/tests/debuginfo/macro-stepping.rs, line 108.
Breakpoint 2 at 0x1338: file /checkout/tests/debuginfo/macro-stepping.rs, line 123.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".

Breakpoint 1, macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:108
108     zzz(); // #break
110     foo!(); // #loc1
#0  macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:110
110     foo!(); // #loc1
112     foo2!(); // #loc2
#0  macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:112
112     foo2!(); // #loc2
114     let x = vec![42]; // #loc3
#0  macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:114
114     let x = vec![42]; // #loc3
macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:114
114     let x = vec![42]; // #loc3
#0  macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:114
114     let x = vec![42]; // #loc3
116     new_scope!(); // #loc4
#0  macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:116
116     new_scope!(); // #loc4
118     println!("Hello {}", // #loc5
#0  macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:118
118     println!("Hello {}", // #loc5
Hello world

Breakpoint 2, macro_stepping::main () at /checkout/tests/debuginfo/macro-stepping.rs:123
123     included(); // #break
macro_stepping::included () at /checkout/tests/debuginfo/macro-stepping.inc:2
2     foo!(); // #inc-loc1
#0  macro_stepping::included () at /checkout/tests/debuginfo/macro-stepping.inc:2
2     foo!(); // #inc-loc1
4     foo2!(); // #inc-loc2
#0  macro_stepping::included () at /checkout/tests/debuginfo/macro-stepping.inc:4
4     foo2!(); // #inc-loc2
6     zzz(); // #inc-loc3
#0  macro_stepping::included () at /checkout/tests/debuginfo/macro-stepping.inc:6
6     zzz(); // #inc-loc3
A debugging session is active.

 Inferior 1 [process 229815] will be killed.

Quit anyway? (y or n) [answered Y; input not from terminal]
------------------------------------------
stderr: none

---- [debuginfo-gdb] tests/debuginfo/macro-stepping.rs stdout end ----

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-perf Status: Waiting on a perf run to be completed. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants