Skip to content

Commit 1fb7c19

Browse files
committed
Fix and improve parallel mode
Parallel mode was crashing with bad memory access due to data races accessing `Frontend.configurationLoader`, more specifically its `cache`. After serializing access (initially with an `NSLock`), I observed that despite not crashing anymore, the performance was surprisingly *worst* than in single threaded mode. That led me down a road of investigating why this was happening, and after some profiling I discovered that `Rule`'s `nameCache` was the main source of contention - causing around 30% of total spent time waiting for locks (`ulock_wait`) on my tests. After replacing the synchronization mechanism on `nameCache` with a more efficient `os_unfair_lock_t` (`pthread_mutex_t` in Linux), the contention dropped significantly, and parallel mode now outperformed single threaded mode as expected. As a bonus, these changes also improved single threaded mode performance as well, due to the reduced overhead of using a lock vs a queue. I then used the same `Lock` approach to serialize access to `Frontend.configurationLoader` which increased the performance gap even further. After these improvements, I was able to obtain quite significant performance gains using `Lock`: - serial (non optimized) vs parallel (optimized): ~5.4x (13.5s vs 74s) - serial (optimized) vs serial (non optimized): ~1.6x (44s vs 74s) - serial (optimized) vs parallel (optimized): ~3.2x (13.5s vs 44s) Sadly, a custom `Lock` implementation is not ideal for `swift-format` to maintain and Windows support was not provided. As such, `NSLock` was used instead which is a part of `Foundation` and supported on all major platforms. Using `NSLock` the improvements were not so good, unfortunately: - serial (non optimized) vs parallel (NSLock): ~1,9x (38s vs 74s) - serial (NSLock) vs serial (non optimized): ~1,4x (52s vs 74s) - serial (NSLock) vs parallel (NSLock): ~1,3x (38s vs 52s) Tests were made on my `MacBookPro16,1` (8-core [email protected]), on a project with 2135 Swift files, compiling `swift-format` in Release mode. ## Changes - Use an `NSLock` to serialize access to `Frontend.configurationLoader`. - Use an `NSLock` to serialize access to `Rule`'s `nameCache` (replacing `nameCacheQueue`)
1 parent e3a4b21 commit 1fb7c19

File tree

2 files changed

+22
-12
lines changed

2 files changed

+22
-12
lines changed

Sources/SwiftFormatCore/Rule.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,22 @@ public protocol Rule {
2828
}
2929

3030
fileprivate var nameCache = [ObjectIdentifier: String]()
31-
fileprivate var nameCacheQueue = DispatchQueue(
32-
label: "com.apple.SwiftFormat.NameCache", attributes: .concurrent)
31+
fileprivate let nameCacheLock = NSLock()
3332

3433
extension Rule {
3534
/// By default, the `ruleName` is just the name of the implementing rule class.
3635
public static var ruleName: String {
3736
let identifier = ObjectIdentifier(self)
38-
let cachedName = nameCacheQueue.sync {
39-
nameCache[identifier]
40-
}
4137

42-
if let cachedName = cachedName {
38+
nameCacheLock.lock()
39+
defer { nameCacheLock.unlock() }
40+
41+
if let cachedName = nameCache[identifier] {
4342
return cachedName
4443
}
4544

4645
let name = String("\(self)".split(separator: ".").last!)
47-
nameCacheQueue.async(flags: .barrier) {
48-
nameCache[identifier] = name
49-
}
50-
46+
nameCache[identifier] = name
5147
return name
5248
}
5349
}

Sources/swift-format/Frontend/Frontend.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ class Frontend {
7878
/// Indicates whether any errors were emitted during execution.
7979
final var errorsWereEmitted: Bool { diagnosticEngine.hasErrors }
8080

81+
/// Lock to serialize access to shared state in parallel mode.
82+
final let lock = NSLock()
83+
8184
/// Creates a new frontend with the given options.
8285
///
8386
/// - Parameter lintFormatOptions: Options that apply during formatting or linting.
@@ -181,7 +184,7 @@ class Frontend {
181184
// loaded. (Do not try to fall back to a path inferred from the source file path.)
182185
if let configurationFilePath = configurationFilePath {
183186
do {
184-
return try configurationLoader.configuration(atPath: configurationFilePath)
187+
return try lock.sync { try configurationLoader.configuration(atPath: configurationFilePath) }
185188
} catch {
186189
diagnosticEngine.diagnose(
187190
Diagnostic.Message(.error, "Unable to read configuration: \(error.localizedDescription)"))
@@ -194,7 +197,8 @@ class Frontend {
194197
if let swiftFilePath = swiftFilePath {
195198
do {
196199
if let configuration =
197-
try configurationLoader.configuration(forSwiftFileAtPath: swiftFilePath)
200+
try lock.sync(
201+
work: { try configurationLoader.configuration(forSwiftFileAtPath: swiftFilePath) })
198202
{
199203
return configuration
200204
}
@@ -213,3 +217,13 @@ class Frontend {
213217
return Configuration()
214218
}
215219
}
220+
221+
private extension NSLock {
222+
223+
@discardableResult
224+
func sync<R>(work: () throws -> R) rethrows -> R {
225+
lock()
226+
defer { unlock() }
227+
return try work()
228+
}
229+
}

0 commit comments

Comments
 (0)