Concurrency Deep Dive

Benchmarking iOS Synchronization: Methodology and Results

January 19, 2026
6 min read
Featured image for blog post: 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:

  1. Warmup - JIT compilation, cache warming, thread pool initialization
  2. Multiple iterations - We run 3x and take the median to reduce noise
  3. Realistic scenarios - Different workloads reveal different characteristics

Test Scenarios

We tested five workload patterns at 50,000 operations each:

ScenarioDescription
Balanced50% reads, 50% writes
Read Heavy90% reads, 10% writes (simulates caches)
Write Heavy90% writes, 10% reads (simulates logging)
Heavy WorkCPU-bound work inside critical section
SerialNo concurrency—pure overhead measurement

The Results

Testing on device with NSLock, os_unfair_lock, OSAllocatedUnfairLock, and Actor:

Balanced (50/50 read/write):

MechanismTimeThroughput
Actor61.9 ms808K ops/s
OSAllocatedUnfairLock167.3 ms299K ops/s
os_unfair_lock171.3 ms292K ops/s
NSLock174.3 ms287K ops/s

Actor is 2.8x faster than the fastest lock under balanced concurrent load.

Read Heavy (90% reads):

MechanismTimeThroughput
Actor64.3 ms778K ops/s
OSAllocatedUnfairLock168.7 ms297K ops/s
NSLock173.7 ms288K ops/s
os_unfair_lock173.8 ms288K ops/s

Actor is 2.7x faster. All locks perform similarly.

Write Heavy (90% writes):

MechanismTimeThroughput
Actor62.7 ms797K ops/s
NSLock162.7 ms307K ops/s
os_unfair_lock168.1 ms298K ops/s
OSAllocatedUnfairLock168.1 ms298K ops/s

Actor is 2.7x faster. Similar pattern to read-heavy.

Heavy Work Inside Lock:

MechanismTimeThroughput
Actor677 ms73.8K ops/s
NSLock4,874 ms10.3K ops/s
OSAllocatedUnfairLock5,041 ms9.9K ops/s
os_unfair_lock5,089 ms9.8K ops/s

Actor is 7.5x faster. Long lock hold times amplify thread contention.

Serial (No Concurrency):

MechanismTimeThroughput
os_unfair_lock10.7 ms4.7M ops/s
OSAllocatedUnfairLock11.2 ms4.5M ops/s
NSLock11.2 ms4.5M ops/s
Serial Queue25.9 ms1.9M ops/s
RW Queue29.2 ms1.7M ops/s
Actor416.2 ms120K 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."

Originally published on deadpixel.llc

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch