Skip to content

Commit 796da7a

Browse files
authored
Merge pull request #83327 from al45tair/eng/PR-156701386
[Concurrency] Fix task switch performance issue.
2 parents 2f658f1 + 85bb057 commit 796da7a

File tree

7 files changed

+154
-14
lines changed

7 files changed

+154
-14
lines changed

lib/SILOptimizer/UtilityPasses/Link.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ class SILLinker : public SILModuleTransform {
105105
SILLinkage::HiddenExternal);
106106
linkUsedFunctionByName("swift_createDefaultExecutors",
107107
SILLinkage::HiddenExternal);
108+
linkUsedFunctionByName("swift_getDefaultExecutor",
109+
SILLinkage::HiddenExternal);
108110
linkEmbeddedRuntimeWitnessTables();
109111
}
110112

stdlib/public/Concurrency/Actor.cpp

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2391,13 +2391,15 @@ static bool mustSwitchToRun(SerialExecutorRef currentSerialExecutor,
23912391
}
23922392

23932393
// else, we may have to switch if the preferred task executor is different
2394-
auto differentTaskExecutor =
2395-
currentTaskExecutor.getIdentity() != newTaskExecutor.getIdentity();
2396-
if (differentTaskExecutor) {
2397-
return true;
2398-
}
2394+
if (currentTaskExecutor.getIdentity() == newTaskExecutor.getIdentity())
2395+
return false;
23992396

2400-
return false;
2397+
if (currentTaskExecutor.isUndefined())
2398+
currentTaskExecutor = swift_getDefaultExecutor();
2399+
if (newTaskExecutor.isUndefined())
2400+
newTaskExecutor = swift_getDefaultExecutor();
2401+
2402+
return currentTaskExecutor.getIdentity() != newTaskExecutor.getIdentity();
24012403
}
24022404

24032405
/// Given that we've assumed control of an executor on this thread,

stdlib/public/Concurrency/Executor.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,13 @@ extension MainActor {
625625
_createDefaultExecutorsOnce()
626626
return _executor!
627627
}
628+
629+
/// An unowned version of the above, for performance
630+
@available(StdlibDeploymentTarget 6.2, *)
631+
static var unownedExecutor: UnownedSerialExecutor {
632+
_createDefaultExecutorsOnce()
633+
return unsafe UnownedSerialExecutor(ordinary: _executor!)
634+
}
628635
}
629636
#endif // os(WASI) || (!$Embedded && !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY)
630637

@@ -643,6 +650,13 @@ extension Task where Success == Never, Failure == Never {
643650
_createDefaultExecutorsOnce()
644651
return _defaultExecutor!
645652
}
653+
654+
/// An unowned version of the above, for performance
655+
@available(StdlibDeploymentTarget 6.2, *)
656+
static var unownedDefaultExecutor: UnownedTaskExecutor {
657+
_createDefaultExecutorsOnce()
658+
return unsafe UnownedTaskExecutor(_defaultExecutor!)
659+
}
646660
}
647661

648662
extension Task where Success == Never, Failure == Never {

stdlib/public/Concurrency/ExecutorBridge.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ namespace swift {
2323
extern "C" SWIFT_CC(swift)
2424
SerialExecutorRef swift_getMainExecutor();
2525

26+
extern "C" SWIFT_CC(swift)
27+
TaskExecutorRef swift_getDefaultExecutor();
28+
2629
#if !SWIFT_CONCURRENCY_EMBEDDED
2730
extern "C" SWIFT_CC(swift)
2831
void *swift_createDispatchEventC(void (*handler)(void *), void *context);

stdlib/public/Concurrency/ExecutorBridge.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,23 @@ internal func _jobGetExecutorPrivateData(
9595
#if !SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
9696
@available(StdlibDeploymentTarget 6.2, *)
9797
@_silgen_name("swift_getMainExecutor")
98-
internal func _getMainExecutorAsSerialExecutor() -> (any SerialExecutor)? {
99-
return MainActor.executor
98+
internal func _getMainExecutorAsSerialExecutor() -> UnownedSerialExecutor {
99+
return unsafe MainActor.unownedExecutor
100100
}
101101
#else
102102
// For task-to-thread model, this is implemented in C++
103103
@available(StdlibDeploymentTarget 6.2, *)
104104
@_silgen_name("swift_getMainExecutor")
105-
internal func _getMainExecutorAsSerialExecutor() -> (any SerialExecutor)?
105+
internal func _getMainExecutorAsSerialExecutor() -> UnownedSerialExecutor
106106
#endif // SWIFT_STDLIB_TASK_TO_THREAD_MODEL_CONCURRENCY
107107
#endif // os(WASI) || !$Embedded
108108

109+
@available(StdlibDeploymentTarget 6.2, *)
110+
@_silgen_name("swift_getDefaultExecutor")
111+
internal func _getDefaultExecutorAsTaskExecutor() -> UnownedTaskExecutor {
112+
return unsafe Task.unownedDefaultExecutor
113+
}
114+
109115
@available(StdlibDeploymentTarget 6.2, *)
110116
@_silgen_name("swift_dispatchMain")
111117
internal func _dispatchMain()

stdlib/public/Concurrency/ExecutorImpl.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,7 @@ internal func donateToGlobalExecutor(
4747
@available(SwiftStdlib 6.2, *)
4848
@_silgen_name("swift_task_getMainExecutorImpl")
4949
internal func getMainExecutor() -> UnownedSerialExecutor {
50-
let executor = _getMainExecutorAsSerialExecutor()
51-
if let executor {
52-
return unsafe executor.asUnownedSerialExecutor()
53-
}
54-
return unsafe unsafeBitCast(executor, to: UnownedSerialExecutor.self)
50+
return _getMainExecutorAsSerialExecutor()
5551
}
5652

5753
@available(SwiftStdlib 6.2, *)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// RUN: %target-run-simple-swift(-Xfrontend -disable-availability-checking -g %import-libdispatch -parse-as-library) | %FileCheck %s
2+
3+
// REQUIRES: concurrency
4+
// REQUIRES: executable_test
5+
6+
// rdar://106849189 move-only types should be supported in freestanding mode
7+
// UNSUPPORTED: freestanding
8+
9+
// UNSUPPORTED: back_deployment_runtime
10+
// REQUIRES: concurrency_runtime
11+
// REQUIRES: synchronization
12+
// REQUIRES: libdispatch
13+
14+
import StdlibUnittest
15+
import Synchronization
16+
import Dispatch
17+
18+
typealias DefaultExecutorFactory = SimpleExecutorFactory
19+
20+
struct SimpleExecutorFactory: ExecutorFactory {
21+
public static var mainExecutor: any MainExecutor {
22+
return SimpleMainExecutor()
23+
}
24+
public static var defaultExecutor: any TaskExecutor {
25+
return SimpleTaskExecutor()
26+
}
27+
}
28+
29+
@available(SwiftStdlib 6.2, *)
30+
final class SimpleMainExecutor: MainExecutor, @unchecked Sendable {
31+
public var isRunning: Bool = false
32+
33+
var shouldStop: Bool = false
34+
let queue = Mutex<[UnownedJob]>([])
35+
36+
func enqueue(_ job: consuming ExecutorJob) {
37+
let unownedJob = UnownedJob(job)
38+
queue.withLock {
39+
$0.append(unownedJob)
40+
}
41+
}
42+
43+
func run() throws {
44+
isRunning = true
45+
while !shouldStop {
46+
let jobs = queue.withLock {
47+
let jobs = $0
48+
$0.removeAll()
49+
return jobs
50+
}
51+
for job in jobs {
52+
job.runSynchronously(on: self.asUnownedSerialExecutor())
53+
}
54+
}
55+
isRunning = false
56+
}
57+
58+
func stop() {
59+
shouldStop = true
60+
}
61+
}
62+
63+
@available(SwiftStdlib 6.2, *)
64+
final class SimpleTaskExecutor: TaskExecutor, @unchecked Sendable {
65+
let queue = Mutex<[UnownedJob]>([])
66+
67+
func enqueue(_ job: consuming ExecutorJob) {
68+
print("Enqueued")
69+
let unownedJob = UnownedJob(job)
70+
queue.withLock {
71+
$0.append(unownedJob)
72+
}
73+
}
74+
75+
func run() throws {
76+
while true {
77+
let jobs = queue.withLock {
78+
let jobs = $0
79+
$0.removeAll()
80+
return jobs
81+
}
82+
for job in jobs {
83+
job.runSynchronously(on: self.asUnownedTaskExecutor())
84+
}
85+
}
86+
}
87+
}
88+
89+
@available(SwiftStdlib 6.2, *)
90+
@main struct Main {
91+
static func main() async {
92+
DispatchQueue.global().async {
93+
try! (Task.defaultExecutor as! SimpleTaskExecutor).run()
94+
}
95+
96+
await withTaskGroup { group in
97+
for _ in 0..<3 {
98+
group.addTask() {
99+
for _ in 0..<100 {
100+
await withUnsafeContinuation { cont in
101+
cont.resume()
102+
}
103+
}
104+
}
105+
}
106+
}
107+
108+
}
109+
}
110+
111+
// If we're on the fast path, we'll only enqueue four times (once per group)
112+
113+
// CHECK: Enqueued
114+
// CHECK: Enqueued
115+
// CHECK: Enqueued
116+
// CHECK: Enqueued
117+
// CHECK-NOT: Enqueued

0 commit comments

Comments
 (0)