Skip to content

Commit a550080

Browse files
committed
[Concurrency] Further improve performance.
Remove some reference counting traffic by using `Unowned*Executor`s. Also, add a test to make sure we stay on the fast path. rdar://156701386
1 parent c696a4b commit a550080

File tree

4 files changed

+136
-10
lines changed

4 files changed

+136
-10
lines changed

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.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,21 +95,21 @@ 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

109109
@available(StdlibDeploymentTarget 6.2, *)
110110
@_silgen_name("swift_getDefaultExecutor")
111-
internal func _getDefaultExecutorAsTaskExecutor() -> (any TaskExecutor)? {
112-
return Task.defaultExecutor
111+
internal func _getDefaultExecutorAsTaskExecutor() -> UnownedTaskExecutor {
112+
return unsafe Task.unownedDefaultExecutor
113113
}
114114

115115
@available(StdlibDeploymentTarget 6.2, *)

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

0 commit comments

Comments
 (0)