-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Avoid moving around large suspended function states in the deferred definition worklist. #5608
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
Conversation
definition worklist. We already go to some effort to avoid moving these, but we end up still moving them twice: once when adding to the worklist and again when reversing a chunk of the worklist. * To avoid a move when constructing the worklist, add an `EmplaceResult` utility that allows the result of a function call to be emplaced into a container. * To avoid moves when reversing the list, stop reversing it. Instead of reversing the list and popping tasks as we run them, we accumulate a sequence of tasks for a deferred definition region, run them in the order they were enqueued, then pop them all at the end. This will in some cases increase the high-water-mark of the size of the worklist, but not asymptotically. The same high-water-mark could be reached with the old approach by reordering the declarations in the source file. In passing, we no longer create `LeaveDeferredDefinitionRegion` tasks for non-nested regions. We don't need them, because we can detect that condition by our reaching the end of the worklist. This means that the enter / leave region actions are now always in correspondence -- we only create them for *nested* regions. The tasks have been renamed to convey this. We still move the suspended function states around if the worklist grows to over 64 entries and gets reallocated. We could potentially address that issue too by switching to a chunked allocation strategy as is used by `ValueStore` and then make the tasks noncopyable, but I'm not attempting that in this PR.
common/emplace_result.h
Outdated
|
||
namespace Carbon { | ||
|
||
// A utility to use when calling an `emplace` function to emplace the result of |
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 am thoroughly confused how this is changing the behaviour of emplace, could you explain it a bit 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.
Extended the comment to explain why this works.
Co-authored-by: Dana Jansens <danakj@orodu.net>
// Worklist is empty: discard the worklist items associated with this | ||
// chunk, and leave the scope. | ||
worklist_.truncate(chunks_.back().first_worklist_index); | ||
context_->decl_name_stack().PopScope(); |
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.
Where is the paired push for this pop? I don't see it in this file, so it seems sufficiently non-local that it could use a comment explaining.
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.
Added a comment. (I'm not really a fan of the division of responsibility here -- some of the scope pushes / pops / suspends / resumes are in the worklist and some are here -- but that's a pre-existing problem that I don't have a good solution to yet. There's probably a better way to factor this functionality.)
common/emplace_result.h
Outdated
// container has made space for the new element, it should not inspect or modify | ||
// the container that is being emplaced into. | ||
template <typename MakeFnT> | ||
class EmplaceResult { |
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.
nit: I might call this EmplaceConstruct as it's what the job of the thing is, and it's not the result of the emplacing operation. Up to you.
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 tied to this particular name, but EmplaceConstruct
seems to be missing some important information -- a reference to the fact that it's calling a function and emplacing the result of that function call. I'd also be happy with things like EmplaceByCalling(callable)
or EmplaceResultOf(callable)
that avoid the possibility of this name being interpreted as "(the) emplace(ment) result" instead of "emplace (the) result (of)".
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.
Both of those would be fine with me, yeah.
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.
OK, I'll go with EmplaceByCalling
-- on further thought I think EmplaceResultOf
sounds too much like a type trait analogous to std::result_of
.
[Edit: now done.]
worklist_.push_back( | ||
LeaveDeferredDefinitionScope{.in_deferred_definition_scope = true}); | ||
CARBON_VLOG("{0}Push LeaveDeferredDefinitionScope (nested)\n", VlogPrefix); | ||
worklist_.emplace_back(LeaveNestedDeferredDefinitionScope{}); |
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 we still want to push_back unless we're using the EmplaceResult tool, don't we?
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.
When adding an element of the same type to a vector, yeah, we should be using push_back
rather than emplace_back
. But the element type of the worklist is a variant, not LeaveNestedDeferredDefinitionScope
. We want an emplace_back
not a push_back
here so that we pass in an (empty) LeaveNestedDeferredDefinitionScope
and the vector calls the variant converting constructor, rather than constructing a (large but almost entirely uninitialized) variant instance on the stack here and a variant copy in the vector push_back
logic.
// If we've not found any deferred definitions in this scope, clean up the | ||
// stack. |
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.
Can we add a bit of explanation about the ==size() vs == size()-1?
I was going to suggest something around PushEnterDeferredDefinitionScope
pushing to worklist_ only in the nested case, but here the size is smaller in the nested case, so it's more complicated/different than that. Why does ==size() mean non-nested and ==size-1 mean nested 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.
Changed the logic to explicitly use the nested
flag instead of effectively recomputing it, and extended comment to explain what's happening.
Make worklist cleanup clearer.
e57ffe7
to
ca6d18f
Compare
Add a comment explaining why we're popping a scope we didn't push.
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.
Thanks, LGTM
…efinition worklist. (carbon-language#5608) We already go to some effort to avoid moving these, but we end up still moving them twice: once when adding to the worklist and again when reversing a chunk of the worklist. * To avoid a move when constructing the worklist, add an `EmplaceResult` utility that allows the result of a function call to be emplaced into a container. * To avoid moves when reversing the list, stop reversing it. Instead of reversing the list and popping tasks as we run them, we accumulate a sequence of tasks for a deferred definition region, run them in the order they were enqueued, then pop them all at the end. This will in some cases increase the high-water-mark of the size of the worklist, but not asymptotically. The same high-water-mark could be reached with the old approach by reordering the declarations in the source file. In passing, we no longer create `LeaveDeferredDefinitionRegion` tasks for non-nested regions. We don't need them, because we can detect that condition by our reaching the end of the worklist. This means that the enter / leave region actions are now always in correspondence -- we only create them for *nested* regions. The tasks have been renamed to convey this. We still move the suspended function states around if the worklist grows to over 64 entries and gets reallocated. We could potentially address that issue too by switching to a chunked allocation strategy as is used by `ValueStore` and then make the tasks noncopyable, but I'm not attempting that in this PR. --------- Co-authored-by: Dana Jansens <danakj@orodu.net>
…efinition worklist. (carbon-language#5608) We already go to some effort to avoid moving these, but we end up still moving them twice: once when adding to the worklist and again when reversing a chunk of the worklist. * To avoid a move when constructing the worklist, add an `EmplaceResult` utility that allows the result of a function call to be emplaced into a container. * To avoid moves when reversing the list, stop reversing it. Instead of reversing the list and popping tasks as we run them, we accumulate a sequence of tasks for a deferred definition region, run them in the order they were enqueued, then pop them all at the end. This will in some cases increase the high-water-mark of the size of the worklist, but not asymptotically. The same high-water-mark could be reached with the old approach by reordering the declarations in the source file. In passing, we no longer create `LeaveDeferredDefinitionRegion` tasks for non-nested regions. We don't need them, because we can detect that condition by our reaching the end of the worklist. This means that the enter / leave region actions are now always in correspondence -- we only create them for *nested* regions. The tasks have been renamed to convey this. We still move the suspended function states around if the worklist grows to over 64 entries and gets reallocated. We could potentially address that issue too by switching to a chunked allocation strategy as is used by `ValueStore` and then make the tasks noncopyable, but I'm not attempting that in this PR. --------- Co-authored-by: Dana Jansens <danakj@orodu.net>
We already go to some effort to avoid moving these, but we end up still moving them twice: once when adding to the worklist and again when reversing a chunk of the worklist.
EmplaceResult
utility that allows the result of a function call to be emplaced into a container.In passing, we no longer create
LeaveDeferredDefinitionRegion
tasks for non-nested regions. We don't need them, because we can detect that condition by our reaching the end of the worklist. This means that the enter / leave region actions are now always in correspondence -- we only create them for nested regions. The tasks have been renamed to convey this.We still move the suspended function states around if the worklist grows to over 64 entries and gets reallocated. We could potentially address that issue too by switching to a chunked allocation strategy as is used by
ValueStore
and then make the tasks noncopyable, but I'm not attempting that in this PR.