-
Notifications
You must be signed in to change notification settings - Fork 343
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 #3846 properly, so that subtrees can be skipped again #4006
base: master
Are you sure you want to change the base?
Conversation
This commit supplies a real fix, which makes retags more complicated, at the benefit of making accesses more performant.
// So we return `true` here, which is definitely correct, but maybe not fully optimal. | ||
// But in cases where we are imprecise, the iteration stops at the next initialized parent anyway, | ||
// so it is likely not a huge loss in practice. | ||
None => true, |
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 still somewhat unhappy with this. In cases where a node is mostly default-initialized, this performance loss here can become significant. A better solution might be to track the "default last foreign access" for not-yet-materialized nodes, similar to how we track the default permission for these. And then update that in here.
Well those are some solid wins. :) (How did you make those graphs?) |
google sheets--the numbers were copied in there manually.
Not really--it's more that #3847 was a solid loss 🙃 this just brings the performance back to where it used to be. |
The previous version of this cheated by being wrong, so it still counts. ;) |
ReservedFrz { conflicted } if prot && !conflicted => None, | ||
// Otherwise, foreign reads do not affect it | ||
ReservedFrz { .. } => Some(AccessKind::Read), | ||
// Famously, ReservedIM survives foreign writes. It is never protected. |
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.
Might we worth a (debug) assertion of !prot
.
/// Returns the strongest foreign action this node survives (without change), | ||
/// where `prot` indicates if it is protected. | ||
/// They are ordered as `None` < `Some(Read)` < `Some(Write)`, in order of power. | ||
pub fn strongest_survivable_foreign_action(&self, prot: bool) -> Option<AccessKind> { |
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 function can't actually be invoked from the outside since PermissionPriv
is private, I think? So please remove the pub
.
/// which states that a node's children survive accesses at least as strong as that node itself. | ||
/// While retags act as a read for the offsets specified in the retag, it is also present at the other | ||
/// offsets, but marked as "lazy." Such lazy nodes can, however, still be affected by foreign accesses. | ||
/// This would violate the invariants that children always survive accesses at least as strong as the last |
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 violate the invariants that children always survive accesses at least as strong as the last | |
/// This would violate the invariant that children always survive accesses at least as strong as the last |
/// recorded foreign access at the parent. | ||
/// To restore the invariant, we need to weaken the information stored in the parent, until it is weak enough | ||
/// to encompass the newly added lazy node. | ||
/// This function compares two accesses, and indicates if the parents needs to be updated. |
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 be either "parents need" or "parent needs"
// if the last access here was a foreign access, but our new child does not | ||
// survive any foreign access, we must let the next foreign access through again |
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.
// if the last access here was a foreign access, but our new child does not | |
// survive any foreign access, we must let the next foreign access through again | |
// If the last access here was a foreign access (`last_recorded` is `Some`), | |
// but our new child does not survive any foreign access (`happening_now`, | |
// is `None`), we must let the next foreign access through again. |
/// See also `reset_last_foreign_access_after_reborrow`. | ||
/// Retags can violate the invariant on `latest_foreign_access`, | ||
/// which states that a node's children survive accesses at least as strong as that node itself. | ||
/// While retags act as a read for the offsets specified in the retag, it is also present at the other |
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 same comment is copied 3 times it seems... seems better to instead reference a common pace where this is explained once.
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.
Indeed. But unfortunately Option<AccessKind>
has no obvious place where one would add such an annotation.
Note that Option<AccessKind>
already has the here-suggested order "natively" since it implements Ord
, and it turns out that the auto-derived order is the correct one here (this is not entirely accidental). But relying on that seems wrong, and the code does not do that currently. Perhaps one could introduce a newtype?
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 relying on that seems wrong,
Why? Your comments treat the type as having an order. And it does even actually have that order. We should do that.
If you feel uneasy relying on it, add a little #[test]
function asserting that the order is the way we want it to be.
// Returns `true` if it changed something, and in that case, we need to continue with the parent. | ||
Some(perm) => perm.reset_last_foreign_access_after_retag(strongest_survivable), | ||
// if there is no permission yet, it is still "default lazy". | ||
// in that case, we would need to compare with that node's original default permission. |
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.
// in that case, we would need to compare with that node's original default permission. | |
// In that case, we would need to compare with that node's original default permission. |
// than the `strongest_survivable` by the newly inserted child. | ||
// Returns `true` if it changed something, and in that case, we need to continue with the parent. | ||
Some(perm) => perm.reset_last_foreign_access_after_retag(strongest_survivable), | ||
// if there is no permission yet, it is still "default lazy". |
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.
// if there is no permission yet, it is still "default lazy". | |
// If there is no permission yet, it is still "default lazy". |
// record whether we did any change (if not, the invariant is restored). | ||
let any_change = self.rperms.iter_mut_all().any(|(_, map)| { | ||
match map.get_mut(current) { | ||
// if there is a permission, ensure that it's latest recorded foreign access is not stronger |
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.
// if there is a permission, ensure that it's latest recorded foreign access is not stronger | |
// If there is a permission, ensure that its latest recorded foreign access is not stronger |
// if there is no permission yet, it is still "default lazy". | ||
// in that case, we would need to compare with that node's original default permission. | ||
// But that depended on whether that node was protected when it was created, which is no | ||
// longer the case 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 don't understand this sentence. What is no longer the case 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.
The protector might have in the meantime gone away. But when it was initially inserted, the "update parent's latest_foreign_access
" logic took the protector into account when it reset that field in the parents. So if the protector is now gone (and in general that information is not even piped through to this point in the code here), we get different results for what the "implicit initial weakening" did, and what state the invariant is in. And at that point, the code becomes too complicated to be maintainable 😛
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 the point is if the protector disappeared the "strongest idempotent access" might become stronger but currently the code doesn't exploit that?
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.
Thinking about it a bit more, I think that without the protector the strongest idempotent foreign access (SIPA) only becomes stronger. Which means that one could (I think) always compute that SIPA at this point with protector := false
, and it's still correct, because it will over-approximate.
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.
without the protector the strongest idempotent foreign access (SIPA) only becomes stronger.
That's easy to test exhaustively.
However, isn't the smaller ("weaker") access the conservative choice? I am confused.
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.
because it's the one that was used to modify that field in the parents, i.e. the one that the invariant relates to. If you now have a new child, you want to make an inference about the parents, whether they're already weak enough for you to stop iterating. So you need to know a lower (or is it upper) bound on the parents, which (in the case of still-to-be-default-initialized fields) is the the thing you used when you inserted it. Which is the SIFA for the permission it had when you inserted it.
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 this is just about stopping the traversal upwards, not about changing anything in what we store in the current node?
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. Currently, the traversal continues. But I'd have hoped one could be smarter here, since this will be hit quite often (basically whenever you only have a reference to a sub-range).
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.
And computing the SIFA based on the current protector state is pointless? Or just not good enough for your perfectionism? ;)
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 also that you'd need to add an extra parameter everywhere to get the current protection state, which would be annoying (in particular since it's only for an imperfect solution)
Issue #3846 was "fixed" by PR #3847, but this fix was a hotfix. After careful thinking about the invariants at play there, the more proper solution is to instead do some extra magic during retags. This PR re-adds the disabled functionality, and ensures that default-lazy new nodes introduced by a retag outside that retag's range do not break anything, by having such retags reset the state that tracks when subtrees can be skipped.
The details can be seen in the code comments.
Here's the difference in running times for the benchmark suite. Note that it's a log graph, since the differences are so large it would otherwise be unreadable: