Benchmarking iOS Synchronization: Methodology and Results

Benchmarking synchronization primitives is tricky. Here's a methodology that produces meaningful results—and what we found on real devices.
Follow along with the code: iOS-Practice on GitHub
Benchmarking Methodology
Good benchmarks need:
- Warmup - JIT compilation, cache warming, thread pool initialization
- Multiple iterations - We run 3x and take the median to reduce noise
- Realistic scenarios - Different workloads reveal different characteristics
Test Scenarios
We tested five workload patterns at 50,000 operations each:
| Scenario | Description |
|---|---|
| Balanced | 50% reads, 50% writes |
| Read Heavy | 90% reads, 10% writes (simulates caches) |
| Write Heavy | 90% writes, 10% reads (simulates logging) |
| Heavy Work | CPU-bound work inside critical section |
| Serial | No concurrency—pure overhead measurement |
The Results
Testing on device with NSLock, os_unfair_lock, OSAllocatedUnfairLock, and Actor:
Balanced (50/50 read/write):
| Mechanism | Time | Throughput |
|---|---|---|
| Actor | 61.9 ms | 808K ops/s |
| OSAllocatedUnfairLock | 167.3 ms | 299K ops/s |
| os_unfair_lock | 171.3 ms | 292K ops/s |
| NSLock | 174.3 ms | 287K ops/s |
Actor is 2.8x faster than the fastest lock under balanced concurrent load.
Read Heavy (90% reads):
| Mechanism | Time | Throughput |
|---|---|---|
| Actor | 64.3 ms | 778K ops/s |
| OSAllocatedUnfairLock | 168.7 ms | 297K ops/s |
| NSLock | 173.7 ms | 288K ops/s |
| os_unfair_lock | 173.8 ms | 288K ops/s |
Actor is 2.7x faster. All locks perform similarly.
Write Heavy (90% writes):
| Mechanism | Time | Throughput |
|---|---|---|
| Actor | 62.7 ms | 797K ops/s |
| NSLock | 162.7 ms | 307K ops/s |
| os_unfair_lock | 168.1 ms | 298K ops/s |
| OSAllocatedUnfairLock | 168.1 ms | 298K ops/s |
Actor is 2.7x faster. Similar pattern to read-heavy.
Heavy Work Inside Lock:
| Mechanism | Time | Throughput |
|---|---|---|
| Actor | 677 ms | 73.8K ops/s |
| NSLock | 4,874 ms | 10.3K ops/s |
| OSAllocatedUnfairLock | 5,041 ms | 9.9K ops/s |
| os_unfair_lock | 5,089 ms | 9.8K ops/s |
Actor is 7.5x faster. Long lock hold times amplify thread contention.
Serial (No Concurrency):
| Mechanism | Time | Throughput |
|---|---|---|
| os_unfair_lock | 10.7 ms | 4.7M ops/s |
| OSAllocatedUnfairLock | 11.2 ms | 4.5M ops/s |
| NSLock | 11.2 ms | 4.5M ops/s |
| Serial Queue | 25.9 ms | 1.9M ops/s |
| RW Queue | 29.2 ms | 1.7M ops/s |
| Actor | 416.2 ms | 120K ops/s |
os_unfair_lock is 39x faster than Actor in serial code.
Why Actors Win Under Concurrency
When you dispatch 50,000 operations to a concurrent queue with locks:
let queue = DispatchQueue(label: "test", attributes: .concurrent)
for i in 0..<50_000 {
queue.async {
container.write(i) // Lock contention!
}
}
GCD spawns threads—lots of them. They all contend for the same lock, burning CPU on context switches and spin-waiting.
Actors use cooperative scheduling:
await withTaskGroup(of: Void.self) { group in
for i in 0..<50_000 {
group.addTask {
await container.write(i)
}
}
}
The Swift runtime limits thread count and manages suspension efficiently. Less contention, more throughput.
Why Locks Win in Serial Code
Without concurrency, locks are just function calls—a few assembly instructions. Actors require task suspension, executor scheduling, and potential context switches. That overhead dominates when there's no contention.
Interview Tip
"I've benchmarked these primitives on device. Actors are 2.5-3x faster under concurrency, and up to 7x faster with heavy work inside the critical section. But in serial code, locks are 39x faster—no async overhead. I profile before choosing."