Optimizing performance of repeated calls of Swift closure from C code

In what I think is a common pattern, I'm trying to interface with some C code that accepts

  1. a handler function that the C code will call and
  2. a pointer to some user-provided data

Here's some (not real) code demonstrating the basic setup. The user creates a Manager class and provides it with a callback. When the user calls Manager.run, some C code is triggered that then calls the user's callback:

class Manager {
    
    private let pointer: SomeCPointer
    
    var userCallback: () -> Void = { }
    
    init() {
        func callback(userData: UnsafeRawPointer) {
            let manager = Unmanaged<Manager>.fromOpaque(userData).takeUnretainedValue()
            
            manager.userCallback()
        }
        
        self.pointer = initSomeCType()
        
        pointer.pointee.callback = callback
        pointer.pointee.userData = Unmanaged.passUnretained(self).toOpaque()
    }
    
    deinit {
        free(pointer)
    }
    
    func run() {
        someCFunction(pointer)
    }
    
}

There's two parts of this code that I want to zoom in on. Manager.run:

func run() {
    someCFunction(pointer)
}

and the function callback:

func callback(userData: UnsafeRawPointer) {
    let manager = Unmanaged<Manager>.fromOpaque(userData).takeUnretainedValue()
    
    manager.userCallback()
}

someCFunction may call the callback any number of times (oftentimes a lot).

So run basically becomes a tight loop of:

let manager = Unmanaged<Manager>.fromOpaque(userData).takeUnretainedValue()

manager.userCallback()

When profiling this code, there's two big penalties around accessing manager and userCallback:

+0x5c	bl                  "DYLD-STUB$$swift_beginAccess"
+0x60	ldp                 x23, x20, [x19, #0x18]
+0x64	mov                 x0, x21
+0x68	bl                  "DYLD-STUB$$swift_bridgeObjectRetain"
+0x6c	mov                 x0, x20
+0x70	bl                  "DYLD-STUB$$swift_retain"
+0x74	add                 x0, sp, #0x60
+0x78	blr                 x23
+0x7c	mov                 x0, x20
+0x80	bl                  "DYLD-STUB$$swift_release"
//  ...
+0xa4	bl                  "DYLD-STUB$$swift_bridgeObjectRelease"
  1. It's wrapped by Swift exclusivity enforcement, and
  2. Retain/release calls

Exclusivity enforcement I could probably eliminate by making userCallback immutable in some fashion (though that comes with its own pains). The retain/release around manager and userCallback though Swift seems to have fewer tools to deal with (and to my surprise none of Unmanaged's APIs actually eliminate retain/release calls from the call site.)

Are there any tools or strategies for this sort of situation that I'm missing?

4 Likes