Skip to content

Commit 21a70e1

Browse files
Fixed quadratic performance of ListMerger when each executed job creates 2+ new jobs of the same priority
See https://forums.swift.org/t/quadratic-performance-of-the-listmerger-in-specific-use-case/69393
1 parent 69f5450 commit 21a70e1

File tree

5 files changed

+89
-58
lines changed

5 files changed

+89
-58
lines changed

include/swift/Basic/ListMerger.h

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#define SWIFT_BASIC_LISTMERGER_H
2020

2121
#include <assert.h>
22+
#include <tuple>
2223

2324
namespace swift {
2425

@@ -62,13 +63,31 @@ namespace swift {
6263
/// to the merger and before being released except by the merger.
6364
template <class Node, class NodeTraits>
6465
class ListMerger {
66+
public:
67+
class LastInsertionPoint {
68+
friend class ListMerger;
69+
Node node = Node();
70+
bool isKnownLastOfEquals = false;
71+
72+
public:
73+
LastInsertionPoint() {}
74+
75+
void nodeWasRemoved(Node removedNode) {
76+
if (node == removedNode) {
77+
*this = LastInsertionPoint();
78+
}
79+
}
80+
};
81+
82+
private:
6583
Node root;
66-
Node lastInsertionPoint = Node();
67-
bool lastInsertionPointIsKnownLastOfEquals = false;
84+
LastInsertionPoint lastInsertionPoint;
85+
6886
public:
6987
/// Construct a merger with the given sorted list as its current list.
70-
ListMerger(Node initialList = Node())
71-
: root(initialList) {}
88+
ListMerger(Node initialList = Node(),
89+
LastInsertionPoint insertionPoint = LastInsertionPoint())
90+
: root(initialList), lastInsertionPoint(insertionPoint) {}
7291

7392
/// Add a single node to this merger's current list.
7493
///
@@ -86,7 +105,7 @@ class ListMerger {
86105
Node stopper = Node();
87106

88107
// If we have a previous insertion point, compare against it.
89-
if (Node lastIP = lastInsertionPoint) {
108+
if (Node lastIP = lastInsertionPoint.node) {
90109
int comparison = NodeTraits::compare(lastIP, newNode);
91110

92111
// If it compares equal, put the new node immediately after the
@@ -169,7 +188,7 @@ class ListMerger {
169188

170189
// If we have a previous insertion point, check for the presumed-common
171190
// case that we're inserting something that should immediately follow it.
172-
if (auto lastIP = lastInsertionPoint) {
191+
if (auto lastIP = lastInsertionPoint.node) {
173192
lastIP = findLastOfEqualsFromLastIP(lastIP);
174193

175194
// Compare against the next node after lastIP, if it exists.
@@ -246,7 +265,7 @@ class ListMerger {
246265

247266
// If we have a previous insertion point, compare the new root
248267
// against it.
249-
if (Node lastIP = lastInsertionPoint) {
268+
if (Node lastIP = lastInsertionPoint.node) {
250269
int comparison = NodeTraits::compare(lastIP, rootOfNewList);
251270

252271
// If it compares equal, we've got an insertion point where
@@ -341,27 +360,27 @@ class ListMerger {
341360

342361
/// Get the current list that's been built up, and clear the internal
343362
/// state of this merger.
344-
Node release() {
345-
Node result = root;
363+
std::tuple<Node, LastInsertionPoint> release() {
364+
auto result = std::make_tuple(root, lastInsertionPoint);
346365
root = Node();
347-
lastInsertionPoint = Node();
366+
lastInsertionPoint = LastInsertionPoint();
348367
return result;
349368
}
350369

351370
private:
352371
/// Set the last point at which we inserted a node, and specify
353372
/// whether we know it was the last in its sequence of equals.
354373
void setLastInsertionPoint(Node lastIP, bool knownEndOfEquals) {
355-
lastInsertionPoint = lastIP;
356-
lastInsertionPointIsKnownLastOfEquals = knownEndOfEquals;
374+
lastInsertionPoint.node = lastIP;
375+
lastInsertionPoint.isKnownLastOfEquals = knownEndOfEquals;
357376
}
358377

359378
/// Given the value of lastInsertionPoint (passed in to avoid
360379
/// reloading it), find the last node in the sequence of equals that
361380
/// contains it.
362381
Node findLastOfEqualsFromLastIP(Node lastIP) const {
363-
assert(lastIP == lastInsertionPoint);
364-
if (!lastInsertionPointIsKnownLastOfEquals)
382+
assert(lastIP == lastInsertionPoint.node);
383+
if (!lastInsertionPoint.isKnownLastOfEquals)
365384
return findLastOfEquals(lastIP);
366385
return lastIP;
367386
}

stdlib/public/Concurrency/Actor.cpp

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,35 @@ class alignas(sizeof(void *) * 2) ActiveActorStatus {
981981
}
982982
};
983983

984+
#if !SWIFT_CONCURRENCY_ACTORS_AS_LOCKS
985+
986+
/// Given that a job is enqueued normally on a default actor, get/set
987+
/// the next job in the actor's queue.
988+
static JobRef getNextJobInQueue(Job *job) {
989+
return *reinterpret_cast<JobRef *>(job->SchedulerPrivate);
990+
}
991+
static void setNextJobInQueue(Job *job, JobRef next) {
992+
*reinterpret_cast<JobRef *>(job->SchedulerPrivate) = next;
993+
}
994+
995+
namespace {
996+
997+
struct JobQueueTraits {
998+
static Job *getNext(Job *job) {
999+
return getNextJobInQueue(job).getAsPreprocessedJob();
1000+
}
1001+
static void setNext(Job *job, Job *next) {
1002+
setNextJobInQueue(job, JobRef::getPreprocessed(next));
1003+
}
1004+
static int compare(Job *lhs, Job *rhs) {
1005+
return descendingPriorityOrder(lhs->getPriority(), rhs->getPriority());
1006+
}
1007+
};
1008+
1009+
} // end anonymous namespace
1010+
1011+
#endif
1012+
9841013
#if SWIFT_CONCURRENCY_ENABLE_PRIORITY_ESCALATION && SWIFT_POINTER_IS_4_BYTES
9851014
#define ACTIVE_ACTOR_STATUS_SIZE (4 * (sizeof(uintptr_t)))
9861015
#else
@@ -1052,10 +1081,13 @@ class DefaultActorImpl : public HeapObject {
10521081
// enforce alignment. This is space that is available for us to use in
10531082
// the future
10541083
alignas(sizeof(ActiveActorStatus)) char StatusStorage[sizeof(ActiveActorStatus)];
1084+
1085+
using ListMerger = swift::ListMerger<Job *, JobQueueTraits>;
1086+
ListMerger::LastInsertionPoint lastInsertionPoint =
1087+
ListMerger::LastInsertionPoint();
10551088
#endif
10561089
// TODO (rokhinip): Make this a flagset
10571090
bool isDistributedRemoteActor;
1058-
10591091
public:
10601092
/// Properly construct an actor, except for the heap header.
10611093
void initialize(bool isDistributedRemote = false) {
@@ -1128,6 +1160,10 @@ class DefaultActorImpl : public HeapObject {
11281160
/// It can be done when actor transitions from Idle to Scheduled or
11291161
/// when actor gets a priority override and we schedule a stealer.
11301162
void scheduleActorProcessJob(JobPriority priority);
1163+
1164+
Job *preprocessQueue(JobRef start);
1165+
Job *preprocessQueue(JobRef unprocessedStart, JobRef unprocessedEnd,
1166+
Job *existingProcessedJobsToMergeInto);
11311167
#endif /* !SWIFT_CONCURRENCY_ACTORS_AS_LOCKS */
11321168

11331169
void deallocateUnconditional();
@@ -1203,31 +1239,6 @@ static NonDefaultDistributedActorImpl *asImpl(NonDefaultDistributedActor *actor)
12031239
/*****************************************************************************/
12041240

12051241
#if !SWIFT_CONCURRENCY_ACTORS_AS_LOCKS
1206-
/// Given that a job is enqueued normally on a default actor, get/set
1207-
/// the next job in the actor's queue.
1208-
static JobRef getNextJobInQueue(Job *job) {
1209-
return *reinterpret_cast<JobRef*>(job->SchedulerPrivate);
1210-
}
1211-
static void setNextJobInQueue(Job *job, JobRef next) {
1212-
*reinterpret_cast<JobRef*>(job->SchedulerPrivate) = next;
1213-
}
1214-
1215-
namespace {
1216-
1217-
struct JobQueueTraits {
1218-
static Job *getNext(Job *job) {
1219-
return getNextJobInQueue(job).getAsPreprocessedJob();
1220-
}
1221-
static void setNext(Job *job, Job *next) {
1222-
setNextJobInQueue(job, JobRef::getPreprocessed(next));
1223-
}
1224-
static int compare(Job *lhs, Job *rhs) {
1225-
return descendingPriorityOrder(lhs->getPriority(), rhs->getPriority());
1226-
}
1227-
};
1228-
1229-
} // end anonymous namespace
1230-
12311242

12321243
// Called with the actor drain lock held
12331244
//
@@ -1240,15 +1251,14 @@ struct JobQueueTraits {
12401251
// and the previous start. We can then process these jobs and merge them into
12411252
// the already processed list of jobs from the previous iteration of
12421253
// preprocessQueue
1243-
static Job *
1244-
preprocessQueue(JobRef unprocessedStart, JobRef unprocessedEnd, Job *existingProcessedJobsToMergeInto)
1245-
{
1254+
Job *DefaultActorImpl::preprocessQueue(JobRef unprocessedStart,
1255+
JobRef unprocessedEnd,
1256+
Job *existingProcessedJobsToMergeInto) {
12461257
assert(existingProcessedJobsToMergeInto != NULL);
12471258
assert(unprocessedStart.needsPreprocessing());
12481259
assert(unprocessedStart.getAsJob() != unprocessedEnd.getAsJob());
12491260

12501261
// Build up a list of jobs we need to preprocess
1251-
using ListMerger = swift::ListMerger<Job*, JobQueueTraits>;
12521262
ListMerger jobsToProcess;
12531263

12541264
// Get just the prefix list of unprocessed jobs
@@ -1263,19 +1273,20 @@ preprocessQueue(JobRef unprocessedStart, JobRef unprocessedEnd, Job *existingPro
12631273
}
12641274

12651275
// Finish processing the unprocessed jobs
1266-
Job *newProcessedJobs = jobsToProcess.release();
1276+
Job *newProcessedJobs = std::get<0>(jobsToProcess.release());
12671277
assert(newProcessedJobs);
12681278

1269-
ListMerger mergedList(existingProcessedJobsToMergeInto);
1279+
ListMerger mergedList(existingProcessedJobsToMergeInto, lastInsertionPoint);
12701280
mergedList.merge(newProcessedJobs);
1271-
return mergedList.release();
1281+
Job *result;
1282+
std::tie(result, lastInsertionPoint) = mergedList.release();
1283+
return result;
12721284
}
12731285

12741286
// Called with the actor drain lock held.
12751287
//
12761288
// Preprocess the queue starting from the top
1277-
static Job *
1278-
preprocessQueue(JobRef start) {
1289+
Job *DefaultActorImpl::preprocessQueue(JobRef start) {
12791290
if (!start) {
12801291
return NULL;
12811292
}
@@ -1288,7 +1299,6 @@ preprocessQueue(JobRef start) {
12881299
// There exist some jobs which haven't been preprocessed
12891300

12901301
// Build up a list of jobs we need to preprocess
1291-
using ListMerger = swift::ListMerger<Job*, JobQueueTraits>;
12921302
ListMerger jobsToProcess;
12931303

12941304
Job *wellFormedListStart = NULL;
@@ -1311,18 +1321,19 @@ preprocessQueue(JobRef start) {
13111321
}
13121322

13131323
// Finish processing the unprocessed jobs
1314-
auto processedJobHead = jobsToProcess.release();
1324+
auto processedJobHead = std::get<0>(jobsToProcess.release());
13151325
assert(processedJobHead);
13161326

13171327
Job *firstJob = NULL;
13181328
if (wellFormedListStart) {
13191329
// Merge it with already known well formed list if we have one.
1320-
ListMerger mergedList(wellFormedListStart);
1330+
ListMerger mergedList(wellFormedListStart, lastInsertionPoint);
13211331
mergedList.merge(processedJobHead);
1322-
firstJob = mergedList.release();
1332+
std::tie(firstJob, lastInsertionPoint) = mergedList.release();
13231333
} else {
13241334
// Nothing to merge with, just return the head we already have
13251335
firstJob = processedJobHead;
1336+
lastInsertionPoint = ListMerger::LastInsertionPoint();
13261337
}
13271338

13281339
return firstJob;
@@ -1528,6 +1539,7 @@ Job * DefaultActorImpl::drainOne() {
15281539
if (_status().compare_exchange_weak(oldState, newState,
15291540
/* success */ std::memory_order_relaxed,
15301541
/* failure */ std::memory_order_relaxed)) {
1542+
lastInsertionPoint.nodeWasRemoved(firstJob);
15311543
SWIFT_TASK_DEBUG_LOG("Drained first job %p from actor %p", firstJob, this);
15321544
traceActorStateTransition(this, oldState, newState, distributedActorIsRemote);
15331545
concurrency::trace::actor_dequeue(this, firstJob);

stdlib/public/Concurrency/CooperativeGlobalExecutor.inc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ static void swift_task_enqueueGlobalImpl(Job *job) {
108108

109109
JobQueueMerger merger(JobQueue);
110110
merger.insert(job);
111-
JobQueue = merger.release();
111+
JobQueue = std::get<0>(merger.release());
112112
}
113113

114114
/// Enqueues a task on the main executor.
@@ -202,7 +202,7 @@ static void recognizeReadyDelayedJobs() {
202202
nextDelayedJob = next;
203203
}
204204

205-
JobQueue = readyJobs.release();
205+
JobQueue = std::get<0>(readyJobs.release());
206206
DelayedJobQueue = nextDelayedJob;
207207
}
208208

utils/test-list-merger/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
SWIFT_SRCROOT=${CURDIR}/../..
22
SRCROOT=${SWIFT_SRCROOT}/..
33
LLVM_SRCROOT=${SRCROOT}/llvm/
4-
LLVM_OBJROOT=${SRCROOT}/build/Ninja-DebugAssert/llvm-macosx-x86_64
4+
LLVM_OBJROOT=${SRCROOT}/build/Ninja-DebugAssert/llvm-macosx-$(shell uname -m)
55

66
HEADERS=${SWIFT_SRCROOT}/include/swift/Basic/ListMerger.h
77

utils/test-list-merger/TestListMerger.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ static void runInsertAndMergeTest(llvm::ArrayRef<Instruction> values) {
161161
assert(!lastMergeEntry && "ended while still building a merge list");
162162

163163
entries.sort(creationOrder);
164-
entries.checkSameAs(merger.release());
164+
entries.checkSameAs(std::get<0>(merger.release()));
165165
}
166166

167167
static void runInsertAtFrontTest(llvm::ArrayRef<unsigned> values) {
@@ -171,7 +171,7 @@ static void runInsertAtFrontTest(llvm::ArrayRef<unsigned> values) {
171171
merger.insertAtFront(entries.create(value));
172172
}
173173
entries.sort(reverseCreationOrder);
174-
entries.checkSameAs(merger.release());
174+
entries.checkSameAs(std::get<0>(merger.release()));
175175
}
176176

177177
static void runConcreteTests() {

0 commit comments

Comments
 (0)