Skip to content

Commit 4449670

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 4449670

File tree

2 files changed

+23
-12
lines changed

2 files changed

+23
-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: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import Foundation
1414
import SwiftFormat
1515
import SwiftFormatConfiguration
16+
import SwiftFormatCore
1617
import SwiftSyntax
1718

1819
class Frontend {
@@ -78,6 +79,9 @@ class Frontend {
7879
/// Indicates whether any errors were emitted during execution.
7980
final var errorsWereEmitted: Bool { diagnosticEngine.hasErrors }
8081

82+
/// Lock to serialize access to shared state in parallel mode.
83+
final let lock = NSLock()
84+
8185
/// Creates a new frontend with the given options.
8286
///
8387
/// - Parameter lintFormatOptions: Options that apply during formatting or linting.
@@ -181,7 +185,7 @@ class Frontend {
181185
// loaded. (Do not try to fall back to a path inferred from the source file path.)
182186
if let configurationFilePath = configurationFilePath {
183187
do {
184-
return try configurationLoader.configuration(atPath: configurationFilePath)
188+
return try lock.sync { try configurationLoader.configuration(atPath: configurationFilePath) }
185189
} catch {
186190
diagnosticEngine.diagnose(
187191
Diagnostic.Message(.error, "Unable to read configuration: \(error.localizedDescription)"))
@@ -194,7 +198,8 @@ class Frontend {
194198
if let swiftFilePath = swiftFilePath {
195199
do {
196200
if let configuration =
197-
try configurationLoader.configuration(forSwiftFileAtPath: swiftFilePath)
201+
try lock.sync(
202+
work: { try configurationLoader.configuration(forSwiftFileAtPath: swiftFilePath) })
198203
{
199204
return configuration
200205
}
@@ -213,3 +218,13 @@ class Frontend {
213218
return Configuration()
214219
}
215220
}
221+
222+
private extension NSLock {
223+
224+
@discardableResult
225+
func sync<R>(work: () throws -> R) rethrows -> R {
226+
lock()
227+
defer { unlock() }
228+
return try work()
229+
}
230+
}

0 commit comments

Comments
 (0)