-
Notifications
You must be signed in to change notification settings - Fork 13k
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
Fix issue #52475: Make loop detector only consider reachable memory #52626
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @petrochenkov (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
I'll have a more detailed look later. Just wondering right now if we can reuse the StableHash stuff of incremental. Seems like we're trying to solve similar issues |
I'm not sure I follow, but reuse sounds good. |
Hmm alloc_id_recursion_tracker should be removed... noone uses it So what I meant was reusing rust/src/librustc/ich/impls_ty.rs Line 392 in fa60b72
|
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 would be clearer if we moved this subset of fields into its own struct within EvalContext
. There's a commit that did this that I reverted in the original PR because those fields were public and I had to change every use of them.
r? @oli-obk |
@ecstatic-morse that's a great point, in fact my idea EDIT: actually I think @oli-obk might be onto something better with |
My understanding of the internals of MIRI are still flaky, so please bear with me if I'm talking nonsense, but after some testing, I'm growing skeptical that the exact example given in #52475 (which I reproduce below) can actually be detected, not because of
Does that make sense? |
Nope, rustc allocates a single variable and just reuses it. if you look at the MIR of that function, you can see that it's even more extreme, because rustc generates a hidden static for the let mut _0: usize; // return place
scope 1 {
let mut _1: &i32; // "x" in scope 1 at src/main.rs:4:9: 4:14
}
scope 2 {
}
let mut _2: &i32;
let mut _3: &i32;
bb0: {
StorageLive(_1); // bb0[0]: scope 0 at src/main.rs:4:9: 4:14
StorageLive(_2); // bb0[1]: scope 0 at src/main.rs:4:23: 4:25
_2 = promoted[1]; // bb0[2]: scope 0 at src/main.rs:4:23: 4:25
// mir::Constant
// + span: src/main.rs:4:23: 4:25
// + ty: &i32
// + literal: promoted[1]
_1 = _2; // bb0[3]: scope 0 at src/main.rs:4:23: 4:25
StorageDead(_2); // bb0[4]: scope 0 at src/main.rs:4:25: 4:26
goto -> bb1; // bb0[5]: scope 1 at src/main.rs:5:5: 7:6
}
bb1: {
StorageLive(_3); // bb1[0]: scope 1 at src/main.rs:6:13: 6:15
_3 = promoted[0]; // bb1[1]: scope 1 at src/main.rs:6:13: 6:15
// mir::Constant
// + span: src/main.rs:6:13: 6:15
// + ty: &i32
// + literal: promoted[0]
_1 = _3; // bb1[2]: scope 1 at src/main.rs:6:9: 6:15
StorageDead(_3); // bb1[3]: scope 1 at src/main.rs:6:15: 6:16
goto -> bb1; // bb1[4]: scope 1 at src/main.rs:5:5: 7:6
} if you change the code to let x = 5;
let mut x: *const u32 = &x;
loop {
let y = 5;
x = &y;
}
0 you get
Where you can see that it keeps taking the address of |
Awesome, thanks for the detailed explanation, I'll investigate later today
why then non`ByRef` locals seem to be preventing the detection, do you have
any clues?
|
Most likely because |
for local in locals.iter() { | ||
// Skip dead locals | ||
if let Some(value) = local { | ||
match value { |
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 you can just call HashStable::hash_stable
on the value to obtain the same effect + simultaneously do the correct thing on ByVal
. Have a look at the HashStable
impl, it should look very much like your code.
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's what I intend to do, I just wanted a proof of concept first to be sure I understand what's going on
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.
So I tried reusing HashStable
for value, however rustc panics:
thread 'main' panicked at 'no value for AllocId', libcore/option.rs:989:5
I guess that means dangling references aren't expected when computing the stable hash, but on the other hand I don't see why that would make the hash not stable. Indeed, hashing the potentially empty result directly does seem to fix the issue.
Does that makes sense?
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.
Yea... That's probably wrong. We should not ICE on
#![feature(const_let)]
const FOO: *const u32 = {
let x = 42;
&x
};
fn main() {
let z = FOO;
}
and similar code. (If we fixed the ICE on the above code, we'd just end up with the ICE you're seeing) So we can just treat dangling pointers as not existing by not hashing them.
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.
Right, so if got what you're saying right, it's safe to fix the implementation of HashStable
for AllocId
such that it doesn't panic and then just reuse it to hash the Frame::locals
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.
yes
Sorry, fat fingered the wrong button on my phone. |
Edit: wait, I think I misunderstood this, isn't what you're saying exactly what line 172 covers? |
Oh I see now, |
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 would be clearer if we moved this subset of fields into its own struct within EvalContext.
I think we should do definitely do this. The amount of handwritten equality code is scary. It looks like we'll accidentally break it in the future.
/// steps *until* the loop detector is enabled. When it is positive, it is | ||
/// the number of steps after the detector has been enabled modulo the loop | ||
/// detector period. | ||
pub(crate) steps_since_detector_enabled: isize, |
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 would murder performance in the regular case. Please undo this commit.
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.
Right, this commit was part of an attempt to detect infinite recursion prior to reaching the stack size limit. The idea was to only snapshot the current frame, which would be much lighter weight, however I soon realized this would obviously lead to false positives and I abandoned the idea, but forgot to revert this commit before pushing this batch.
In any case I have already reverted this on my local workspace.
} | ||
} | ||
|
||
impl<'a, 'b, 'mir, 'tcx, M> HashStable<StableHashingContext<'b>> for EvalSnapshot<'a, 'mir, 'tcx, M> | ||
impl<'a, 'mir, 'tcx, M> HashStable<StableHashingContext<'a>> for EvalSnapshot<'mir, 'tcx, M> |
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.
You should be able to use the hash stable derive macro 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.
The macro seems to hardcode the lifetime parameters, is there a straightforward way to extend it for multiple generic parameters?
Indeed, I still didn't remove the [WIP] notice because I'm still working out a way to simplify this algorithm, which is proving harder than I first envisioned. My current idea is to resolve all Allocations reachable through locals in the stack into some custom structure that would be trivial to compara, however I'm kinda caught up on the distinction between the allocations map in |
Also the current implementation pushed is broken, because it doesn't actually clone the Allocations. |
The implementation of HashStable uses the former, whereas the original infonite loop detection algorithm uses the latter, which one should I use? Oh no. I forgot about this. Most of the impls are on If we can make the impls generic, then we can just use |
I'm not sure this would be so straightforward, the allocation map seems to come from a global variable rather than the hashing context, that is, Based on that observation, my current implementation also resolves allocation using the global context, as opposed to |
The global context allocations are statics and constants, while the ones in the EvalContext are for computing the current static/constant. cc @michaelwoerister do you think we can tune HashStable impls by making all the trait HashingContext {
fn with_allocation<T>(&self, f: impl FnOnce(&Allocation) -> T) -> T;
} that allow reusing that code? |
EDIT: I think I misread this the first time, what do you mean by current static/constant? |
I reset the branch to a state I'm reasonably confident to be correct. Now I really need help understanding what exactly needs to be done moving forward with the implementation of To summarize my understanding, all Open questions:
|
@bors r+ |
📌 Commit 05cdf8d has been approved by |
Fix issue #52475: Make loop detector only consider reachable memory As [suggested](#51702 (comment)) by @oli-obk `alloc_id`s should be ignored by traversing all `Allocation`s in interpreter memory at a given moment in time, beginning by `ByRef` locals in the stack. - [x] Generalize the implementation of `Hash` for `EvalSnapshot` to traverse `Allocation`s - [x] Generalize the implementation of `PartialEq` for `EvalSnapshot` to traverse `Allocation`s - [x] Commit regression tests Fixes #52626 Fixes #52849
☀️ Test successful - status-appveyor, status-travis |
@brunocodutra @oli-obk I must be missing something... I am staring at this code and trying to understand how it avoids going in endless cycles when there are cycles in memory. However, I cannot see anything here that would stop copying when we reach an allocation that has already been copied. Has that been overlooked, or am I just missing the place where the magic is happening? |
Does stable hashing avoid cycles? I thought it did not and concluded one couldn't form cycles of allocations, at least not in const fn. Can you come up with a counter exemple that hangs the compiler? |
How would stable hashing avoid cycles...? I don't understand how it would even begin to have the infrastructure and state for that.^^
I am sure this will be easy once we got a few more features stabilizied. Still trying to get one with the current restricted set. Another thing: Why does |
I refer to stable hashing because not only it's part of loop detection, but also pretty much how the snapshot itself is implemented. As for the other question, I can't think of a good reason besides me being obtuse. I'll revisit this PR later this evening with a fresh mind and much better understanding of the language - this PR was the very first thing I ever wrote in Rust. |
Sure but it has nothing to do with detecting cycles.^^ And actually, it's not just cycles. Currently this duplicates allocations when they are referenced multiple times. So imagine
This will duplicate the original assertion (just containing
That's okay. :) I was just wondering if there is maybe something I missed. I am touching the snapshot stuff in #54380, and I will see if I can change it to snapshot earlier and not copy all of memory. I won't do any deduplication though. |
Hm actually stable hashing does something with |
I give up. I tried making a snapshot early, but I cannot comprehend what this code does, and I really should get actual work done.^^ The problem I ran into is that the snapshot isn't actually a snapshot. It's not a deep copy, it's full of references to... somewhere. This definitely needs lots of documentation, right now, the code is incomprehensible as far as I am concerned. |
I opened #54384 to track the lack of documentation. It would be great if you could take care of that @brunoabinader while the memory of this PR is still fresh in your mind :) And sorry if I came across too complaining. I wanted to get some other work done and the loop detector made that so much more complicated, so I wanted to quickly fix it to be simpler and got frustrated when that didn't work and I couldn't figure out why. I shouldn't have loaded that frustration on you though. I am happy for your contributions! |
At a high level, what it does is to crawl up the allocation stack and
replace every weak reference (AllocId) by a strong shared reference, so
that a trivial equality comparison does the right thing, comparing the
actual allocations rather than their IDs.
I see now why we clone everything and use a reference at the time we
compare for equality: it's to avoid duplicating clones. Imagine the same
allocation is referred through its AllocId in several places. If we crawl
up the allocation structure and replace every AllocId by a clone of the
allocation, we will clone the same thing over and over again. Instead we
clone all allocations only once and then just map AllocIds to references by
the time we need to compare the state for equality. Does that make sense?
This is actually not super expensive, the equality comparison needs to
crawl up the structure either way, the only extra cost is querying Memory,
which is a trade off to avoid duplicating clones.
|
As suggested by @oli-obk
alloc_id
s should be ignored by traversing allAllocation
s in interpreter memory at a given moment in time, beginning byByRef
locals in the stack.Hash
forEvalSnapshot
to traverseAllocation
sPartialEq
forEvalSnapshot
to traverseAllocation
sFixes #52626
Fixes #52849