Search code examples
objective-cswiftnsdictionary

Accessing object-c NSDictionary values in Swift is slow


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.

Expensive call stack

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


Solution

  • 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).