Help understanding how Swift handles mutation of types bridged from Objective-C

I'm trying to better understand how Swift handles the mutation of types bridged from Objective-C, specifically as it relates to copy-on-write vs. in-place mutation.

From what I have observed so far, it looks like Swift only bridges the immutable versions of Foundation collection types to their equivalent Swift types. For example,

@interface MyClass : NSObject

@property (nonatomic) NSArray<NSNumber*>* myArray;

@end

would get imported into Swift as...

class MyClass: NSObject {

var myArray: Array<NSNumber>

}

but

@interface MyClass : NSObject

@property NSMutableArray<NSNumber*>* myArray;

@end

would get imported as....

class MyClass: NSObject {

var myArray: NSMutableArray

}

This poses a problem — if I want to vend arrays from Objective-C that can be mutated in-place from Swift code, I can't. I'm stuck with NSArray, which means that every time I mutate myArray from Swift, Swift will create a copy, mutate that copy, then replace the existing array with the copy, which is slower than the in-place mutation I could perform from Objective-C by calling addObject: on an NSMutableArray.

My questions are:

  1. Am I correct about how this model works? (If I want myArray bridged as a Swift Array, I must always use NSArray, and this means the copy-mutate-write dance)
  2. Doesn't this make bridged Objective-C APIs slower than they might otherwise be?
  3. Is there a way for me to have myArray bridged as a Swift Array and also be able to mutate myArray in-place from Swift?

Thanks!

Yea from a high level perspective, what you have is spot on. There are obviously some finer details than just that - e.g. lazy bridging versus eager bridging and copy on write semantics, but generally speaking your analysis right.

In some cases yes, but more often than not no - the performance side of things is pretty good with the copy on write semantics and lazy bridging behaviors. With any software there are definitely spots that can always be tweaked to eek out an extra ounce of perf. For many apps this often doesn't even show up on their overall performance.

Not really - that would defeat some of the performance optimizations present in swift and also lead you down some dark corridors of the inner workings of things that might not be ideal.

It might be good to consider what isolation of concerns should be done. E.g. an object vending out a mutable array may be susceptible to races or data inconsistencies. Instead you might be better off having methods to add or remove objects keeping the MyClass as the controller of truth and only allow it to vend an immutable NSArray (worth noting that -[NSMutableArray copy] will actually create a copy on write container so that is super efficient).

I would also suggest that if you do make the MyClass the controller of truth then changing the property to be @property (nonatomic, readonly, copy) might be good. Alternatively if that isn't the goal then I would suggest the best practice would be to make sure to have the property @property (nonatomic, copy) since that would lead to better consistency when doing things from swift like:

myObject.myArray[3] = NSNumber(3) etc

Thanks for the reply!

How thorough are these optimizations? To give an example, wouldn't adding objects in a loop from Swift be substantially slower due to copy-on-write? i.e. for _ in 0..<100_000 { myObject.array.append(random()) } Is this a scenario Swift has optimizations for?

I'm thinking I'll have to do just this; thanks for the suggestion.

Yup, repeatedly bridging in a loop is definitely a potential source of performance problems. Without being able to "see into" the ObjC property's implementation, swiftc likely has to conservatively assume it could be doing something exciting in the getter method and not hoist the call out of the loop.

One optimization I've contemplated in the past is adding a "bridgeForMutationAtIndex" entry point to the runtime, with the idea being that we could then avoid bridging collection contents that are never looked at (though lazy bridging should take care of this in many cases).

Taking advantage of mutability on the ObjC side would also be interesting, but might require more compiler heroics than I'm personally comfortable with (e.g. recognizing that "bridge-then-append" can be losslessly transformed into "objc_msgSend appendObject:"). That said, I'm on the standard library team not the compiler team, so I can't speak for them :slight_smile:

1 Like

@David_Smith This is really interesting stuff, thanks for sharing more background. Makes me wish there was a way to indicate these kinds of things to Swift explicitly as a language user (or that Swift was a bit more upfront about some of these penalties)