Skip to content

Commit 09b4d06

Browse files
zygoloiddanakj
authored andcommitted
Avoid moving around large suspended function states in the deferred definition 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>
1 parent 2a1f6c9 commit 09b4d06

File tree

8 files changed

+263
-120
lines changed

8 files changed

+263
-120
lines changed

common/BUILD

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,21 @@ cc_library(
116116
hdrs = ["concepts.h"],
117117
)
118118

119+
cc_library(
120+
name = "emplace_by_calling",
121+
hdrs = ["emplace_by_calling.h"],
122+
)
123+
124+
cc_test(
125+
name = "emplace_by_calling_test",
126+
srcs = ["emplace_by_calling_test.cpp"],
127+
deps = [
128+
":emplace_by_calling",
129+
"//testing/base:gtest_main",
130+
"@googletest//:gtest",
131+
],
132+
)
133+
119134
cc_library(
120135
name = "enum_base",
121136
hdrs = ["enum_base.h"],

common/emplace_by_calling.h

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
2+
// Exceptions. See /LICENSE for license information.
3+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
5+
#ifndef CARBON_COMMON_EMPLACE_BY_CALLING_H_
6+
#define CARBON_COMMON_EMPLACE_BY_CALLING_H_
7+
8+
#include <type_traits>
9+
#include <utility>
10+
11+
namespace Carbon {
12+
13+
// A utility to use when calling an `emplace` function to emplace the result of
14+
// a function call. Expected usage is:
15+
//
16+
// my_widget_vec.emplace_back(EmplaceByCalling([&] {
17+
// return ConstructAWidget(...);
18+
// }));
19+
//
20+
// In this example, the result of `ConstructAWidget` will be constructed
21+
// directly into the new element of `my_widget_vec`, without performing a copy
22+
// or move.
23+
//
24+
// Note that the type of the argument to `emplace_back` is an `EmplaceByCalling`
25+
// instance, not the type `DestT` stored in the container. When the `DestT`
26+
// instance is eventually initialized directly from the `EmplaceByCalling`, a
27+
// conversion function on `EmplaceByCalling` is used that converts to the type
28+
// `DestT` being emplaced. This `DestT` initialization does not call an
29+
// additional `DestT` copy or move constructor to initialize the result, and
30+
// instead initializes it in-place in the container's storage, per the C++17
31+
// guaranteed copy elision rules. Similarly, within the conversion function, the
32+
// result is initialized directly by calling `make_fn`, again relying on
33+
// guaranteed copy elision.
34+
//
35+
// Because the make function is called from the conversion function,
36+
// `EmplaceByCalling` should only be used in contexts where it will be used to
37+
// initialize a `DestT` object exactly once. This is generally true of `emplace`
38+
// functions. Also, because the `make_fn` callback will be called after the
39+
// container has made space for the new element, it should not inspect or modify
40+
// the container that is being emplaced into.
41+
template <typename MakeFnT>
42+
class EmplaceByCalling {
43+
public:
44+
explicit(false) EmplaceByCalling(MakeFnT make_fn)
45+
: make_fn_(std::move(make_fn)) {}
46+
47+
// Convert to the exact return type of the make function, by calling the make
48+
// function to construct the result. No implicit conversions are permitted
49+
// here, as that would mean we are not constructing the result in place.
50+
template <typename DestT>
51+
requires std::same_as<DestT, std::invoke_result_t<MakeFnT&&>>
52+
// NOLINTNEXTLINE(google-explicit-constructor)
53+
explicit(false) operator DestT() && {
54+
return std::move(make_fn_)();
55+
}
56+
57+
private:
58+
MakeFnT make_fn_;
59+
};
60+
61+
template <typename MakeFnT>
62+
EmplaceByCalling(MakeFnT) -> EmplaceByCalling<MakeFnT>;
63+
64+
} // namespace Carbon
65+
66+
#endif // CARBON_COMMON_EMPLACE_BY_CALLING_H_

common/emplace_by_calling_test.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
2+
// Exceptions. See /LICENSE for license information.
3+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
5+
#include "common/emplace_by_calling.h"
6+
7+
#include <gtest/gtest.h>
8+
9+
#include <list>
10+
11+
namespace Carbon {
12+
namespace {
13+
14+
struct NoncopyableType {
15+
NoncopyableType() = default;
16+
NoncopyableType(const NoncopyableType&) = delete;
17+
auto operator=(const NoncopyableType&) -> NoncopyableType& = delete;
18+
};
19+
20+
auto Make() -> NoncopyableType { return NoncopyableType(); }
21+
22+
TEST(EmplaceByCalling, Noncopyable) {
23+
std::list<NoncopyableType> list;
24+
// This should compile.
25+
list.emplace_back(EmplaceByCalling(Make));
26+
}
27+
28+
TEST(EmplaceByCalling, NoncopyableInAggregate) {
29+
struct Aggregate {
30+
int a, b, c;
31+
NoncopyableType noncopyable;
32+
};
33+
34+
std::list<Aggregate> list;
35+
// This should compile.
36+
list.emplace_back(EmplaceByCalling(
37+
[] { return Aggregate{.a = 1, .b = 2, .c = 3, .noncopyable = Make()}; }));
38+
}
39+
40+
class CopyCounter {
41+
public:
42+
explicit CopyCounter(int* counter) : counter_(counter) {}
43+
CopyCounter(const CopyCounter& other) : counter_(other.counter_) {
44+
++*counter_;
45+
}
46+
47+
private:
48+
int* counter_;
49+
};
50+
51+
TEST(EmplaceByCalling, NoCopies) {
52+
std::vector<CopyCounter> vec;
53+
vec.reserve(10);
54+
int copies = 0;
55+
for (int i = 0; i != 10; ++i) {
56+
vec.emplace_back(EmplaceByCalling([&] { return CopyCounter(&copies); }));
57+
}
58+
EXPECT_EQ(0, copies);
59+
}
60+
61+
} // namespace
62+
} // namespace Carbon

toolchain/check/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ cc_library(
177177
":diagnostic_emitter",
178178
":dump",
179179
"//common:check",
180+
"//common:emplace_by_calling",
180181
"//common:error",
181182
"//common:find",
182183
"//common:map",

toolchain/check/deferred_definition_worklist.cpp

Lines changed: 48 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
#include <algorithm>
88
#include <optional>
9+
#include <variant>
910

11+
#include "common/emplace_by_calling.h"
1012
#include "common/vlog.h"
1113
#include "toolchain/base/kind_switch.h"
1214
#include "toolchain/check/handle.h"
@@ -27,8 +29,10 @@ auto DeferredDefinitionWorklist::SuspendFunctionAndPush(
2729
Parse::FunctionDefinitionStartId node_id) -> void {
2830
// TODO: Investigate factoring out `HandleFunctionDefinitionSuspend` to make
2931
// `DeferredDefinitionWorklist` reusable.
30-
worklist_.push_back(CheckSkippedDefinition{
31-
index, HandleFunctionDefinitionSuspend(context, node_id)});
32+
worklist_.emplace_back(EmplaceByCalling([&] {
33+
return CheckSkippedDefinition{
34+
index, HandleFunctionDefinitionSuspend(context, node_id)};
35+
}));
3236
CARBON_VLOG("{0}Push CheckSkippedDefinition {1}\n", VlogPrefix, index.index);
3337
}
3438

@@ -37,96 +41,68 @@ auto DeferredDefinitionWorklist::PushEnterDeferredDefinitionScope(
3741
bool nested = !entered_scopes_.empty() &&
3842
entered_scopes_.back().scope_index ==
3943
context.decl_name_stack().PeekInitialScopeIndex();
40-
entered_scopes_.push_back({.worklist_start_index = worklist_.size(),
44+
entered_scopes_.push_back({.nested = nested,
45+
.worklist_start_index = worklist_.size(),
4146
.scope_index = context.scope_stack().PeekIndex()});
42-
worklist_.push_back(EnterDeferredDefinitionScope{
43-
.suspended_name = std::nullopt, .in_deferred_definition_scope = nested});
44-
CARBON_VLOG("{0}Push EnterDeferredDefinitionScope {1}\n", VlogPrefix,
45-
nested ? "(nested)" : "(non-nested)");
47+
if (nested) {
48+
worklist_.emplace_back(EmplaceByCalling([&] {
49+
return EnterNestedDeferredDefinitionScope{.suspended_name = std::nullopt};
50+
}));
51+
CARBON_VLOG("{0}Push EnterDeferredDefinitionScope (nested)\n", VlogPrefix);
52+
} else {
53+
// Don't push a task to re-enter a non-nested scope. Instead,
54+
// SuspendFinishedScopeAndPush will remain in the scope when executing the
55+
// worklist tasks.
56+
CARBON_VLOG("{0}Entered non-nested deferred definition scope\n",
57+
VlogPrefix);
58+
}
4659
return !nested;
4760
}
4861

4962
auto DeferredDefinitionWorklist::SuspendFinishedScopeAndPush(Context& context)
5063
-> FinishedScopeKind {
51-
auto start_index = entered_scopes_.pop_back_val().worklist_start_index;
64+
auto [nested, start_index, _] = entered_scopes_.pop_back_val();
5265

53-
// If we've not found any deferred definitions in this scope, clean up the
54-
// stack.
55-
if (start_index == worklist_.size() - 1) {
66+
// If we've not found any tasks to perform in this scope, clean up the stack.
67+
// For non-nested scope, there will be no tasks on the worklist for this scope
68+
// in this case; for a nested scope, there will just be a task to re-enter the
69+
// nested scope.
70+
if (!nested && start_index == worklist_.size()) {
5671
context.decl_name_stack().PopScope();
57-
auto enter_scope =
58-
get<EnterDeferredDefinitionScope>(worklist_.pop_back_val());
59-
CARBON_VLOG("{0}Pop EnterDeferredDefinitionScope (empty)\n", VlogPrefix);
60-
return enter_scope.in_deferred_definition_scope
61-
? FinishedScopeKind::Nested
62-
: FinishedScopeKind::NonNestedEmpty;
72+
CARBON_VLOG("{0}Left non-nested empty deferred definition scope\n",
73+
VlogPrefix);
74+
return FinishedScopeKind::NonNestedEmpty;
75+
}
76+
if (nested && start_index == worklist_.size() - 1) {
77+
CARBON_CHECK(std::holds_alternative<EnterNestedDeferredDefinitionScope>(
78+
worklist_.back()));
79+
worklist_.pop_back();
80+
context.decl_name_stack().PopScope();
81+
CARBON_VLOG("{0}Pop EnterNestedDeferredDefinitionScope (empty)\n",
82+
VlogPrefix);
83+
return FinishedScopeKind::Nested;
6384
}
6485

6586
// If we're finishing a nested deferred definition scope, keep track of that
6687
// but don't type-check deferred definitions now.
67-
if (auto& enter_scope =
68-
get<EnterDeferredDefinitionScope>(worklist_[start_index]);
69-
enter_scope.in_deferred_definition_scope) {
88+
if (nested) {
89+
auto& enter_scope =
90+
get<EnterNestedDeferredDefinitionScope>(worklist_[start_index]);
7091
// This is a nested deferred definition scope. Suspend the inner scope so we
7192
// can restore it when we come to type-check the deferred definitions.
72-
enter_scope.suspended_name = context.decl_name_stack().Suspend();
93+
enter_scope.suspended_name.emplace(
94+
EmplaceByCalling([&] { return context.decl_name_stack().Suspend(); }));
7395

7496
// Enqueue a task to leave the nested scope.
75-
worklist_.push_back(
76-
LeaveDeferredDefinitionScope{.in_deferred_definition_scope = true});
77-
CARBON_VLOG("{0}Push LeaveDeferredDefinitionScope (nested)\n", VlogPrefix);
97+
worklist_.emplace_back(LeaveNestedDeferredDefinitionScope{});
98+
CARBON_VLOG("{0}Push LeaveNestedDeferredDefinitionScope\n", VlogPrefix);
7899
return FinishedScopeKind::Nested;
79100
}
80101

81-
// We're at the end of a non-nested deferred definition scope. Prepare to
82-
// start checking deferred definitions. Enqueue a task to leave this outer
83-
// scope and end checking deferred definitions.
84-
worklist_.push_back(
85-
LeaveDeferredDefinitionScope{.in_deferred_definition_scope = false});
86-
CARBON_VLOG("{0}Push LeaveDeferredDefinitionScope (non-nested)\n",
87-
VlogPrefix);
88-
89-
// We'll process the worklist in reverse index order, so reverse the part of
90-
// it we're about to execute so we run our tasks in the order in which they
91-
// were pushed.
92-
std::reverse(worklist_.begin() + start_index, worklist_.end());
93-
94-
// Pop the `EnterDeferredDefinitionScope` that's now on the end of the
95-
// worklist. We stay in that scope rather than suspending then immediately
96-
// resuming it.
97-
CARBON_CHECK(
98-
holds_alternative<EnterDeferredDefinitionScope>(worklist_.back()),
99-
"Unexpected task in worklist.");
100-
worklist_.pop_back();
101-
CARBON_VLOG("{0}Handle EnterDeferredDefinitionScope (non-nested)\n",
102-
VlogPrefix);
102+
// We're at the end of a non-nested deferred definition scope. Start checking
103+
// deferred definitions.
104+
CARBON_VLOG("{0}Starting deferred definition processing\n", VlogPrefix);
103105
return FinishedScopeKind::NonNestedWithWork;
104106
}
105107

106-
auto DeferredDefinitionWorklist::Pop(
107-
llvm::function_ref<auto(Task&&)->void> handle_fn) -> void {
108-
if (vlog_stream_) {
109-
CARBON_KIND_SWITCH(worklist_.back()) {
110-
case CARBON_KIND(const CheckSkippedDefinition& definition):
111-
CARBON_VLOG("{0}Handle CheckSkippedDefinition {1}\n", VlogPrefix,
112-
definition.definition_index.index);
113-
break;
114-
case CARBON_KIND(const EnterDeferredDefinitionScope& enter):
115-
CARBON_CHECK(enter.in_deferred_definition_scope);
116-
CARBON_VLOG("{0}Handle EnterDeferredDefinitionScope (nested)\n",
117-
VlogPrefix);
118-
break;
119-
case CARBON_KIND(const LeaveDeferredDefinitionScope& leave): {
120-
bool nested = leave.in_deferred_definition_scope;
121-
CARBON_VLOG("{0}Handle LeaveDeferredDefinitionScope {1}\n", VlogPrefix,
122-
nested ? "(nested)" : "(non-nested)");
123-
break;
124-
}
125-
}
126-
}
127-
128-
handle_fn(std::move(worklist_.back()));
129-
worklist_.pop_back();
130-
}
131-
132108
} // namespace Carbon::Check

0 commit comments

Comments
 (0)