-
Notifications
You must be signed in to change notification settings - Fork 348
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.
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, this seems to be the issue in zip-equal
where a better handling of this makes things go 28% more brrr.
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. ;) |
// 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)
I pushed some quite large changes, which restructure a lot and rewrite most of the comments. Please have a look at it again, starting in the new file |
bbbec3a
to
a7f5fac
Compare
/// the entire range, which can violate this invariant without explicitly causing a local access which would reset things. | ||
/// So this needs to be done manually, thus `ensure_no_stronger_than`, and also the test `permission_sifa_is_correct` in `perms.rs` | ||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)] | ||
pub enum LastForeignAccess { |
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 is this the "last foreign access" or the SIFA? You seem to be using these two terms interchangeably. That's confusing, please pick one and use it consistently.
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 felt uncomfortable with using strongest_idempotent_foreign_access
in field names, can I shorten it to sifa
? Apart from that, the field used to be called latest_foreign_access
but has not changed its semantics. So you're saying it should have had a different name already a year ago?
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 interested in discussing which name it should have had a year ago. ;)
The "strongest" part isn't actually relevant for correctness, is it? So IMO "idempotent_foreign_access" should suffice. "local access" doesn't make sense as a variant name, so I would propose
enum IdempotentForeignAccess { None, Read, Write }
let access = perm.strongest_idempotent_foreign_access(prot); | ||
// We now assert it is idempotent, and never causes UB. | ||
// First, if the SIFA includes foreign reads, assert it is idempotent under foreign reads. | ||
if LastForeignAccess::ForeignRead <= access { |
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 LastForeignAccess::ForeignRead <= access { | |
if access >= LastForeignAccess::ForeignRead { |
same below
// We now assert it is idempotent, and never causes UB. | ||
// First, if the SIFA includes foreign reads, assert it is idempotent under foreign reads. | ||
if LastForeignAccess::ForeignRead <= access { | ||
assert_eq!(perm, transition::perform_access(AccessKind::Read, AccessRelatedness::DistantAccess, perm, prot).unwrap()); |
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.
What's a "DistantAccess"? Is that the same as "foreign"? If yes, we should rename. (Can be a separate PR.)
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, it's a cousin access. The AccessRelatedness
enum has 4 constructors (child, self, cousin, parent), but for almost all cases, two of them (child+self, cousin+parent) are treated the same. The only exception is in protector end writes.
But here we need to choose a concrete inhabitant. We could also pick Parent
, i.e. a different member of the same equivalence class.
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.
Ah, we should rename to CousinAccess
then.
And please add a comment here explaining what you just said.
latest_foreign_access: Option<AccessKind>, | ||
/// See `foreign_access_skipping.rs` | ||
/// Stores the strongest idempotent foreign access for this location. | ||
/// Invariant is that at all children, the strongest idempotent foreign access is stronger (greater). |
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 is an approximation, right? It's okay for this to always be LocalAccess
even if it would be idempotent under foreign reads? That should be documented.
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 always OK for this field to be LocalAccess
. Also for the one in Node
. That would be as if this whole optimization did not exist.
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.
Okay, then please explain that. Currently the definition of SIFA makes it sound like an exact bound.
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 the strongest it can be because the stronger it is the more subtrees we skip -> the faster it is
better now^^? |
@rustbot ready |
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: