Skip to content

Use OSAllocatedUnfairLock to avoid heap allocation in UnfairLock #149

Description

@hfutrell

Summary

UnfairLock (the Darwin path in BezierKit/Library/Lock.swift) backs os_unfair_lock with a manually heap-allocated pointer. We should switch to OSAllocatedUnfairLock, which holds the lock storage inline (no separate heap allocation) and removes the manual pointer lifecycle.

Current code

BezierKit/Library/Lock.swift:11-26:

#if canImport(Darwin)
internal final class UnfairLock {
    private let lockPointer: UnsafeMutablePointer<os_unfair_lock>
    init() {
        lockPointer = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)  // heap allocation
        lockPointer.initialize(to: os_unfair_lock())
    }
    deinit {
        lockPointer.deallocate()
    }
    func sync<T>(_ f: () throws -> T) rethrows -> T {
        os_unfair_lock_lock(lockPointer)
        defer { os_unfair_lock_unlock(lockPointer) }
        return try f()
    }
}
#else
internal final class UnfairLock {
    private let lock = NSLock()
    ...
}
#endif

The os_unfair_lock must not be copied (the lock is identified by its address), which is why it is pinned behind an UnsafeMutablePointer and explicitly allocate/deallocated. That's an extra heap allocation per lock plus manual lifetime management.

Proposal

Use OSAllocatedUnfairLock on platforms where it's available. It owns the lock storage internally (no caller-managed heap pointer), is Sendable, and is the Apple-recommended replacement for raw os_unfair_lock. It also offers a withLock { } API that maps directly onto our sync { }.

Availability: OSAllocatedUnfairLock requires iOS 16 / macOS 13 / tvOS 16 / watchOS 9. For older Apple OS versions, fall back to the existing raw-os_unfair_lock implementation; non-Darwin (Linux/WASM) keeps the NSLock path. Sketch:

#if canImport(Darwin)
import os
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
    // OSAllocatedUnfairLock<Void>, withLock { f() }
} else {
    // existing UnsafeMutablePointer<os_unfair_lock> path
}
#else
// NSLock path (unchanged)
#endif

(An @available-gated type/typealias is cleaner than a runtime branch on the hot path; the exact shape is an implementation detail.)

Verification

  • Confirm in the release-build assembly that the lock no longer triggers a per-instance heap allocation.
  • Confirm no regression in lock acquire/release cost (it should be equivalent — OSAllocatedUnfairLock wraps the same primitive).
  • Keep behavior identical on the pre-iOS-16 and non-Darwin fallbacks.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions