I've encountered a performance issue bridging between swift and objective-c. I'm trying to fully understand whats going on behind the scenes so that I can avoid it in the future.
I have an objc type Car
@interface Car
@property (nonatomic,readonly) NSDictionary<NSString *, Part *> *parts;
@end
Then in swift I enumerate over a huge amount of Car
objects and look up a Part
from each cars parts
dictionary.
for(var car in allCarsInTheWorld) {
let part = car.parts["partidstring"] //This is super slow.
}
Overall the loop above in my working app takes about 5-10 seconds. I can work around the problem by modifying the above code like below, which results in the same loop running in milliseconds:
Fixed obj-c file
@interface Car
@property (nonatomic,readonly) NSDictionary<NSString *, Part *> *parts;
// Look up the part from the parts dictionary above
// in obj-c implementation and return it
-(Part *)partFor:(NSString *)partIdString;
@end
Fixed Swift file
for(var car in allCarsInTheWorld) {
let part = car.partFor("partidstring") //This is fast, performance issue gone.
}
Whats the cause of the performance dip? The only thing I can think is that the obj-c dictionary is being copied when I access it from swift.
Edit: Added picture of profiling stack. These are the frames as I call into the dictionary. Seems like it has to do with the string rather than the dictionary itself.
This seems to be the closest match to the issue I can find: https://forums.swift.org/t/string-performance-how-to-ensure-fast-path-for-comparison-hashing/27436
A part of the problem is that bridging NSDictionary<NSString *, Part *>
to [String: Part]
involves runtime checks for all keys and values of the dictionary. This is needed because the Objective-C generics arguments for NSDictionary
don't guarantee that the dictionary won't held incompatible keys/values (for example Objective-C code could add non-string keys or non-Part values to the dictionary). And for large amounts of dictionaries this can become time consuming.
Another aspect is that Swift will also likely create a corresponding dictionary to make it immutable, since the Objective-C one might as well be a `NSMutableDictionary'. This involves extra allocations and deallocations.
Your approach of adding a partFor()
function avoids the above two by keeping the dictionary hidden from the Swift world. And it's also better architecturally speaking, as you are hiding the implementation details for the storage of the car parts (assuming you also make the dictionary private).