You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Runtime code is generated for functions only used in const eval #119214
I expected to see this happen: Both check1 and check2 should compile to functions that just use a compiletime-evaluated struct value
Instead, this happened: check1 disapeared from the assembly, and it looks like all the code needed to runtime-evaluate it got added instead
The weird thing is that the function that regressed is the one that uses a const item by name, rather than the one that just calls a const function inside a non-const function body.
Version it worked on
It most recently worked on: 1.74.1 (stable as of writing this)
Note that adding #[no_mangle] to check1 to force it to appear in the binary does cause it to appear as a constant. I think this might be a case of "in isolation, codegen is weird, you have to evaluate it at a whole program level"?
This looks like another instance of #116505, meaning that depending on some heuristics leaf functions are only codegenned in cgu that need them. Since check1 and check2 are not called, they are not seen. This is good for optimizations but quite unfortunate for inspecting asm on Compiler Explorer.
Adding #[no_mangle] will force them to be available, you can also add -C link-dead-code=yes, or turn of the optimization with -Z cross-crate-inline-threshold=never on nightly.
I think there are multiple things happening here. I'll try to explain what I'm seeing.
I expected to see this happen: Both check1 and check2 should compile to functions that just return a compiletime-evaluated struct value
That's not possible, those functions both take runtime parameters. I'm presuming you mean "use a compiletime-evaluated struct value" instead?
check1 disapeared from the assembly
This is the cross-crate-inlining effect; I've been mulling over ideas to improve the situation, but this is not a bug. It's very unfortunate though. compiler-explorer/compiler-explorer#5782
and it looks like all the code needed to runtime-evaluate it got added instead... The weird thing is that the function that regressed is the one that uses a const item by name, rather than the one that just calls a const function inside a non-const function body.
Since automatic cross-crate-inlining landed, the assembly generated for this crate contains Thing::new and a bunch of symbols related to panics. Which looks very goofy. At first I was afraid that I broke something in that PR (#116505) related to reachability, but I'm now quite sure I didn't. On an old compiler, if you just add #[inline] to fn oof, you'll get the goofy assembly in godbolt: https://godbolt.org/z/q4E6s4jvs
The change here is that fn oof is cross-crate-inlinable, which means that reachability analysis must consider the contents of its body to be reachable outside this CGU. This is a dark corner of the whole "automatic #[inline]" or "cross-crate-inlinable" approach that I thought I had dodged in the original PR by only inferring cross-crate-inlinability for leaf functions. Turns out I didn't. In this program, fn oof is a leaf function in MIR because it looks like this:
But to reachability analysis, this function has now made the initializer of that const reachable. Ergo, to reachability analysis this is not a leaf function.
So cross-crate-inlining turns the surface area of this crate from "a symbol called oof that I can call and nothing else" to "a symbol called oof and also its body and everything reachable from it". So the compiler dutifully recurses through the const fn that initializes this const and instantiates everything that it reaches.
So from one perspective, this is sensible. But from the OP's, this is obviously silly. All this extra panic code was made reachable from compile time evaluation, but we stamped out runtime code for it. So I wonder if that's what we should be tracking here; if reachability determines something is reachable but only at compile time.
Okay, so if I understand correctly we still don't have an explanation for why these even get added to the monomorphization set. More research is needed...
FWIW this doesn't reproduce on latest nightly any more, the #[inline] has to be added again (making current nightly behave like old versions): https://godbolt.org/z/4vxsT4cT3
So is_reachable_non_generic switches from false to true. That boils down to a membership test on the set computed here, which is created by starting with the reachable_set computed here and then removing some stuff. reachable_set in turn recurses into inline functions here and into the MIR body of constants here.
So... I guess the question is, is reachable_set supposed to return only things that are runtime-reachable (in which case it is currently returning too much), or is it supposed to return everything that reachable in any fashion (in which case the collector using this as its criterion is wrong since there we only want to collect runtime-relevant things).
So from one perspective, this is sensible. But from the OP's, this is obviously silly. All this extra panic code was made reachable from compile time evaluation, but we stamped out runtime code for it. So I wonder if that's what we should be tracking here; if reachability determines something is reachable but only at compile time.
I think what happens is that since T is reachable (which is correct), it may be the case that T returns a function pointer to any function it mentions, recursively. So just in case T returns a function pointer to Thing::new, we mark Thing::new reachable.
Now, we could probably skip this for top-level const items, since we know we can always evaluate them to a value, and then we can just check the final value for any function pointers and only codegen those. (And the collector already has logic to find these function pointers.)
But for associated consts, this is a lot harder. I think we'd have to keep track (inside reachable computation) of whether the current item is const-reachable or full-reachable, and then if we are const-reachable that does not in general get added to the regular reachable set, until we find a function item mentioned in a way that it does not immediately get called -- those are then added as regular-reachable again. Then there are still examples where we'd generate runtime code for no reason but it'd be a lot less common. Fundamentally we can't fully avoid false positives here as we can't be sure which functions may be referenced via function pointers that actually leak into the final value of the const (and thus potentially into runtime code)
we'd have to keep track (inside reachable computation) of whether the current item is const-reachable or full-reachable, and then if we are const-reachable that does not in general get added to the regular reachable set, until we find a function item mentioned in a way that it does not immediately get called -- those are then added as regular-reachable again.
Ah! I think this might explain why my attempt to implement const-reachable didn't work.
Do you still have that branch somewhere? What I imagine would be completely internal to reachable.rs. At least according to the comments there, we encode ctfe MIR for everything anyway, so it's only runtime reachability we have to worry about.
But, surprisingly, a const item can make things runtime reachable.
When a runtime function calls a private const fn, we don't have to consider that function to be const-reachable. So the naive approach outlined above could overestimate how much is const-reachable. The two reachable sets do interact though... I think it's a fixed point of a computation that goes like
All public functions and statics are runtime-reachable.
All public const fn and const are const-reachable.
If a runtime-reachable function that needs cross-crate MIR mentions some function or static, that other item is also runtime-reachable.
If a runtime-reachable function that needs cross-crate MIR mentions a const, that item is const-reachable.
If a const-reachable item mentions an item, that item is const-reachable.
If a const-reachable item mentions a function/static in a way that the function item itself (or a function pointer computed from it) could escape the current item, then that function/static is runtime-reachable.
Okay so that computed two sets as result from the query. I don't know if that's necessary.
Regarding the analysis outlined above, I assume it is pretty rare that a private function is const for no reason, so it's probably okay to simplify this a bit:
All public items are runtime-reachable.
If a runtime-reachable function that needs cross-crate MIR mentions some item, that other item is also runtime-reachable.
If a runtime-reachable item is a const or const fn, it is const-reachable.
If a const-reachable item mentions an item, that item is const-reachable.
If a const-reachable item mentions a function/static in a way that the function item itself (or a function pointer computed from it) could escape the current item, then that function/static is runtime-reachable.
Activity
asquared31415 commentedon Dec 22, 2023
Note that adding
#[no_mangle]tocheck1to force it to appear in the binary does cause it to appear as a constant. I think this might be a case of "in isolation, codegen is weird, you have to evaluate it at a whole program level"?RossSmyth commentedon Dec 22, 2023
This looks like another instance of #116505, meaning that depending on some heuristics leaf functions are only codegenned in cgu that need them. Since check1 and check2 are not called, they are not seen. This is good for optimizations but quite unfortunate for inspecting asm on Compiler Explorer.
Adding
#[no_mangle]will force them to be available, you can also add-C link-dead-code=yes, or turn of the optimization with-Z cross-crate-inline-threshold=neveron nightly.https://godbolt.org/z/4rfGGrbWx
saethlin commentedon Dec 23, 2023
I think there are multiple things happening here. I'll try to explain what I'm seeing.
That's not possible, those functions both take runtime parameters. I'm presuming you mean "use a compiletime-evaluated struct value" instead?
This is the cross-crate-inlining effect; I've been mulling over ideas to improve the situation, but this is not a bug. It's very unfortunate though. compiler-explorer/compiler-explorer#5782
This I think is a bug. I've made a smaller example here: https://godbolt.org/z/GoEa8jc5f and I'm going to look into it...
saethlin commentedon Dec 23, 2023
The reproducer is this:
Since automatic cross-crate-inlining landed, the assembly generated for this crate contains
Thing::newand a bunch of symbols related to panics. Which looks very goofy. At first I was afraid that I broke something in that PR (#116505) related to reachability, but I'm now quite sure I didn't. On an old compiler, if you just add#[inline]tofn oof, you'll get the goofy assembly in godbolt: https://godbolt.org/z/q4E6s4jvsThe change here is that
fn oofis cross-crate-inlinable, which means that reachability analysis must consider the contents of its body to be reachable outside this CGU. This is a dark corner of the whole "automatic #[inline]" or "cross-crate-inlinable" approach that I thought I had dodged in the original PR by only inferring cross-crate-inlinability for leaf functions. Turns out I didn't. In this program,fn oofis a leaf function in MIR because it looks like this:But to reachability analysis, this function has now made the initializer of that
constreachable. Ergo, to reachability analysis this is not a leaf function.So cross-crate-inlining turns the surface area of this crate from "a symbol called
oofthat I can call and nothing else" to "a symbol calledoofand also its body and everything reachable from it". So the compiler dutifully recurses through theconst fnthat initializes this const and instantiates everything that it reaches.So from one perspective, this is sensible. But from the OP's, this is obviously silly. All this extra panic code was made reachable from compile time evaluation, but we stamped out runtime code for it. So I wonder if that's what we should be tracking here; if reachability determines something is reachable but only at compile time.
17 remaining items
RalfJung commentedon Mar 12, 2024
RalfJung commentedon Mar 13, 2024
FWIW this doesn't reproduce on latest nightly any more, the
#[inline]has to be added again (making current nightly behave like old versions): https://godbolt.org/z/4vxsT4cT3RalfJung commentedon Mar 13, 2024
Turns out reachability is involved, I think. Just very indirectly.
The key difference in the log of the good behavior (without
#[inline]) vs the bad behavior (with#[inline]) isIOW, this function suddenly considers
Thing::newto be a root:rust/compiler/rustc_monomorphize/src/collector.rs
Lines 1293 to 1307 in d3514a0
So
is_reachable_non_genericswitches fromfalsetotrue. That boils down to a membership test on the set computed here, which is created by starting with thereachable_setcomputed here and then removing some stuff.reachable_setin turn recurses into inline functions here and into the MIR body of constants here.So... I guess the question is, is
reachable_setsupposed to return only things that are runtime-reachable (in which case it is currently returning too much), or is it supposed to return everything that reachable in any fashion (in which case the collector using this as its criterion is wrong since there we only want to collect runtime-relevant things).Auto merge of rust-lang#122371 - oli-obk:visit_nested_body, r=tmiasko
RalfJung commentedon May 8, 2024
So, looking at the comments I wrote in #122769...
I think what happens is that since
Tis reachable (which is correct), it may be the case thatTreturns a function pointer to any function it mentions, recursively. So just in caseTreturns a function pointer toThing::new, we markThing::newreachable.Now, we could probably skip this for top-level const items, since we know we can always evaluate them to a value, and then we can just check the final value for any function pointers and only codegen those. (And the collector already has logic to find these function pointers.)
But for associated consts, this is a lot harder. I think we'd have to keep track (inside
reachablecomputation) of whether the current item is const-reachable or full-reachable, and then if we are const-reachable that does not in general get added to the regular reachable set, until we find a function item mentioned in a way that it does not immediately get called -- those are then added as regular-reachable again. Then there are still examples where we'd generate runtime code for no reason but it'd be a lot less common. Fundamentally we can't fully avoid false positives here as we can't be sure which functions may be referenced via function pointers that actually leak into the final value of the const (and thus potentially into runtime code)saethlin commentedon May 8, 2024
Ah! I think this might explain why my attempt to implement const-reachable didn't work.
RalfJung commentedon May 8, 2024
RalfJung commentedon May 8, 2024
When a runtime function calls a private
const fn, we don't have to consider that function to be const-reachable. So the naive approach outlined above could overestimate how much is const-reachable. The two reachable sets do interact though... I think it's a fixed point of a computation that goes likeconst fnandconstare const-reachable.saethlin commentedon May 9, 2024
Yes, though I expect there isn't anything in it worth salvaging: master...saethlin:rust:const-reachability
RalfJung commentedon May 9, 2024
Okay so that computed two sets as result from the query. I don't know if that's necessary.
Regarding the analysis outlined above, I assume it is pretty rare that a private function is
constfor no reason, so it's probably okay to simplify this a bit:Rollup merge of rust-lang#124904 - RalfJung:reachable, r=tmiasko
Unrolled build for rust-lang#124904
Rollup merge of #124904 - RalfJung:reachable, r=tmiasko
Auto merge of rust-lang#122505 - oli-obk:visit_nested_body2, r=tmiasko
RalfJung commentedon Jun 11, 2024
On nightly, godbolt doesn't show any assembly any more with the original reproducer, thanks to #122505.
However, this variant still generates assembly: