-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Use a DFS to compare CTFE snapshots #66946
Use a DFS to compare CTFE snapshots #66946
Conversation
56a760c
to
077a810
Compare
/// Erase any allocation IDs. | ||
/// | ||
/// Used when comparing heap snapshots. | ||
pub fn erase_id(self) -> Pointer<Tag, ()> { |
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.
Please rename to erase_alloc_id
.
But isn't it easier to just say ptr1.offset == ptr2.offset
? Pointers with AllocId
type ()
are kind of pointless (all puns intended).
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.
Oh, you are recursively doing through even for Place
. But how can that make any sense, given that all AllocIds are considered equal at this point...?
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.
👍 to erase_alloc_id
.
This is the part that makes AllocId
s compare as equal. It means we can use the derived PartialEq
impl. The alternative is to overload AllocIdInvariant
for more types, which would also be fine.
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.
But why does it make any sense to compare just the offsets?
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.
Other code is responsible for comparing the allocation that each AllocId
references, specifically the PartialEq
implementation of InterpRef
. Am I misunderstanding the question?
src/librustc_mir/const_eval.rs
Outdated
if ecx.machine.loop_detector.is_empty() { | ||
// FIXME(#49980): make this warning a lint | ||
ecx.tcx.sess.span_warn(span, | ||
"Constant evaluating a complex constant, this might take some time"); | ||
} |
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.
Why did you move this out? Seems like a layering violation for this code to care about "emptiness" of the loop detector.
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.
Oh I see, you could remove the tcx
and span
arguments then. That is a good argument, but is_empty
is not a great name.
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_been_invoked
?
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.
No that's negated... is_unused
?
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_been_invoked
is negated but is_unused
isn't?
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.
is_being_invoked_the_first_time()
🙃
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_been_invoked is negated but is_unused isn't?
Well I wanted to avoid the !
is all. It's way too easy to miss.
type Item = FrameSnapshot<'a, 'tcx>; | ||
/// A wrapper type for the various interpreter data structures that compares them without | ||
/// considering the numeric value of `AllocId`s. | ||
struct AllocIdInvariant<'a, T>(&'a T); |
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.
"invariant" is an odd term here, that indicates that something doesn't change.
IgnoreAllocId
sounds more accurate? But I have no idea why it even makes any sense to do this.^^
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.
Yeah, I would not be opposed to reverting to the AllocId
-sensitive method, or removing this entirely. It is pretty cool when you accidentally omit a break
and you're told there's a provably infinite loop in your program, and I have some sentimental connection to this code since it was my first real rust PR. However, I think a configurable instruction limit (with a low default) coupled with a real lint would have basically the same effect.
Once loops are implemented, people will actually start hitting bugs/performance issues in this code. I wanted to make sure I understood what's going on now. I'm worried people will see long-running constant evaluations grind to a halt because of this machinery and have no way to turn it off.
cc @oli-obk
This means we have a dangling pointer, right? |
let id = stack.pop()?; | ||
let alloc = memory.get_raw(id); | ||
|
||
if let Ok(alloc) = alloc { |
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.
Is this where you are ignoring InterpError
? This seems reasonable.
We should make sure that we don't catch errors requiring allocation though, similar to this. Maybe create a new helper method at the error type for this?
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 not sure what you mean by "errors requiring allocation". Are there certain errors that we should bug
on instead of ignoring? Is Ub
one of these?
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.
Any error type that has a String
or other heap types in their fields will allocate when being thrown, and the catching will then destroy the allocated values. If this allocate->throw->catch->destroy happens in a high frequency we're wasting a lot of cycles with all the allocating/deallocating.
So, the idea is to make sure that any error that is caught and acted upon (and not just rethrown) does not allocate
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.
Idea: Should we make the set of errors that Memory
can emit a separate type (with appropriate Into
impls) so we know the set of errors that Memory
methods can emit and can make sure all of them don't allocate?
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.
That makes sense. I'll probably wait for someone else to do this? It feels like y'all have a decent idea of what you want here.
// dependencies. | ||
let bytes = self.inspect_with_undef_and_ptr_outside_interpreter(all_bytes); | ||
live_allocs.map(|(_, alloc)| alloc.ok().map(AllocIdInvariant)) | ||
.eq(other_live_allocs.map(|(_, alloc)| alloc.ok().map(AllocIdInvariant))) |
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.
@RalfJung. I discard the contents of the InterpError
with ok
. Do you think we can differentiate between null pointers, pointers to ZSTs and dangling pointers by looking at InterpError
like you mentioned above? Do ZST pointers have zero-sized backing allocations?
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 started a review, and forgot about it. I'll continue it later. For now just dumping the little status it has
src/librustc_mir/const_eval.rs
Outdated
if ecx.machine.loop_detector.is_empty() { | ||
// FIXME(#49980): make this warning a lint | ||
ecx.tcx.sess.span_warn(span, | ||
"Constant evaluating a complex constant, this might take some time"); | ||
} |
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.
is_being_invoked_the_first_time()
🙃
4027611
to
baac4af
Compare
//! | ||
//! ```rust,ignore(const-loop) | ||
//! const fn inf_loop() { | ||
//! // Function call to prevent promotion. |
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.
inside const fns promotion is not prevented for const fns, also memoization will memoize this 🙃
You'll want const fn zeros(_: &i32) -> [isize; 4] { [0; 4] }
and pass a local variable as a reference at the call site.
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.
Doh! Can't believe I missed this. My worry is that the test for issue 52475 isn't really creating new AllocId
s, since the &0
is getting promoted out.
This method is based on `erase_tag`, and replaces `AllocId`s with `()`.
Co-Authored-By: Oliver Scherer <[email protected]>
a168d70
to
afb4faa
Compare
☔ The latest upstream changes (presumably #67216) made this pull request unmergeable. Please resolve the merge conflicts. |
What is the long-term plan here? From other conversation, it seems like one plan was to remove the loop checker entirely. So we probably should finish that discussion before spending more time on this PR? Also I got quite a backlog of PRs to handle so @oli-obk if you could take over review that would be much appreciated. |
//! Instead of comparing `AllocId`s between snapshots, we first map `AllocId`s in each snapshot to | ||
//! their DFS index, then compare those instead. |
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.
That makes sense. However, what I would have expected then is to see Pointer<Tag, DfsIndex>
instead of Pointer<Tag, AllocId>
, and then one would compare those. What I still don't get is why we have a way to compare pointers ignoring the AllocId
. That means we must have a magic scheme somewhere to figure out all the AllocId
we ignored and compare their DfsIndex
instead? That sounds quite round-about and fragile, on first sight. (I didn't read the code, though.)
r? @oli-obk |
This was discussed in https://rust-lang.zulipchat.com/#narrow/stream/146212-t-compiler.2Fconst-eval/topic/Time.20limits.20during.20const-eval/near/183588492 and from the last comment
I am assuming that we can close this PR |
…op-detector, r=RalfJung Remove const eval loop detector Now that there is a configurable instruction limit for CTFE (see rust-lang#67260), we can replace the loop detector with something much simpler. See rust-lang#66946 for more discussion about this. Although the instruction limit is nightly-only, the only practical way to reach the default limit uses nightly-only features as well (although CTFE will still execute code using such features inside an array initializer on stable). This will at the very least require a crater run, since it will result in an error wherever the "long running const eval" warning appeared before. We may need to increase the default for `const_eval_limit` to work around this. Resolves rust-lang#54384 cc rust-lang#49980 r? @oli-obk cc @RalfJung
…op-detector, r=RalfJung Remove const eval loop detector Now that there is a configurable instruction limit for CTFE (see rust-lang#67260), we can replace the loop detector with something much simpler. See rust-lang#66946 for more discussion about this. Although the instruction limit is nightly-only, the only practical way to reach the default limit uses nightly-only features as well (although CTFE will still execute code using such features inside an array initializer on stable). This will at the very least require a crater run, since it will result in an error wherever the "long running const eval" warning appeared before. We may need to increase the default for `const_eval_limit` to work around this. Resolves rust-lang#54384 cc rust-lang#49980 r? @oli-obk cc @RalfJung
This is an alternative fix to #52475 that hopes to address some parts of #54384.
The major development in this PR is the use of an actual depth-first search to compare the heap between snapshots, instead of the current recursive method. We treat the set of pointers on the stack as an ordered list of "roots". Using this as the initial search stack, a DFS can enumerate all reachable memory in a predictable order. This is important, since without a stable ordering, we would need to compute an isomorphism for the allocation graph of each snapshot to compare them.
Equality between snapshots is checked by iterating in lock-step over their allocation graphs and comparing each
Allocation
using anIgnoreAllocId
wrapper with customPartialEq
andHash
impls. If the allocations are equivalent but forAllocId
s, all pointers in the allocation are mapped to a DFS index and then compared.I find the existing implementation pretty inscrutable, so I'm not equipped to speculate on performance differences. At the very least, my method will use less memory when a collision does occur, since we no longer clone
Memory
in full, just the reachable portions.Some aspects of #54384 remain unaddressed, most notably what to do when we fail to resolve an
AllocId
. My PR takes what I believe is the same approach as the existing implementation; it converts theInterpError
into aNone
and continues traversing the allocation graph. I don't know what the correct behavior would be.r? @RalfJung
cc @oli-obk