在 Swift 中访问 object-c NSDictionary 值很慢

Posted

技术标签:

【中文标题】在 Swift 中访问 object-c NSDictionary 值很慢【英文标题】:Accessing object-c NSDictionary values in Swift is slow 【发布时间】:2019-08-19 11:02:42 【问题描述】:

我遇到了 swift 和 Objective-c 之间的性能问题。我试图完全了解幕后发生的事情,以便将来避免它。

我有一个 objc 类型 Car

@interface Car 
  @property (nonatomic,readonly) NSDictionary<NSString *, Part *> *parts;
@end

然后我迅速列举了大量的Car 对象并从每辆汽车parts 字典中查找Part

for(var car in allCarsInTheWorld) 
  let part = car.parts["partidstring"] //This is super slow. 

在我的工作应用程序中,上面的整个循环大约需要 5-10 秒。我可以通过修改上面的代码来解决这个问题,如下所示,这会导致相同的循环在毫秒内运行:

固定 obj-c 文件

@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

固定的 Swift 文件

for(var car in allCarsInTheWorld) 
  let part = car.partFor("partidstring") //This is fast, performance issue gone. 

性能下降的原因是什么?我唯一能想到的是,当我从 swift 访问 obj-c 字典时,它正在被复制。

编辑:添加了分析堆栈的图片。这些是我在字典中调用的框架。似乎它与字符串而不是字典本身有关。

这似乎是我能找到的最接近问题的匹配:https://forums.swift.org/t/string-performance-how-to-ensure-fast-path-for-comparison-hashing/27436

【问题讨论】:

您是否尝试过使用 Instruments (Time Profiler) 分析代码? – 这里也有类似的观察:***.com/q/46833459. 【参考方案1】:

部分问题在于将NSDictionary&lt;NSString *, Part *&gt; 桥接到[String: Part] 涉及对字典的所有键和值进行运行时检查。这是必需的,因为 NSDictionary 的 Objective-C 泛型参数不保证字典不会包含不兼容的键/值(例如,Objective-C 代码可以将非字符串键或非 Part 值添加到字典)。对于大量的字典,这可能会变得很耗时。

另一方面是 Swift 也可能会创建一个相应的字典以使其不可变,因为 Objective-C 的字典也可能是一个 `NSMutableDictionary'。这涉及额外的分配和解除分配。

您添加 partFor() 函数的方法通过将字典隐藏在 Swift 世界中来避免上述两种情况。而且它在架构上也更好,因为您隐藏了汽车零件存储的实现细节(假设您还将字典设为私有)。

【讨论】:

感谢您的解释。如此简单,完全有道理。我学到了一些新东西:)【参考方案2】:

问题似乎是在您的第一个版本中,在循环的每次迭代中,整个字典都被转换为 swift 字典。

代码的行为如下:

for(var car in allCarsInTheWorld) 
  let car_parts = car.parts as [String: Part] // This is super slow.
  let part = car_parts["partidstring"] 

这是 Swift 桥接在这里工作方式的一种错误特征。如果 Swift 编译器只调用 Objective C 方法 -objectForKeyedSubscript: 会快得多。

在此之前,如果您关心性能,那么实现像 -partFor: 这样的自定义 objc 方法是一个很好的解决方案。

【讨论】:

【参考方案3】:

假设你展示了一个 ObjC 类:

@interface MCADictionaryHolder : NSObject

@property (nonatomic) NSDictionary<NSString *, id> * _Nonnull objects;

- (void)randomise:(NSInteger)upperBound;
- (id _Nullable)itemForKey:(NSString * _Nonnull)key;

@end

@implementation MCADictionaryHolder

- (instancetype)init

   self = [super init];
   if (self) 
      self.objects = [[NSDictionary alloc] init];
   
   return self;


-(void)randomise:(NSInteger)upperBound 
   NSMutableDictionary * d = [[NSMutableDictionary alloc] initWithCapacity:upperBound];
   for (NSInteger i = 0; i < upperBound; i++) 
      NSString *inStr = [@(i) stringValue];
      [d setObject:inStr forKey:inStr];
   
   self.objects = [[NSDictionary alloc] initWithDictionary:d];


-(id)itemForKey:(NSString *)key 
   id value = [self.objects objectForKey:key];
   return  value;


@end

假设您启动了类似于如下所示的性能测试:

class NSDictionaryToDictionaryBridgingTests: LogicTestCase 
   
   func test_default() 
      let bound = 2000
      let keys = (0 ..< bound).map  "\($0)" 
      var mutableDict: [String: Any] = [:]
      for key in keys 
         mutableDict[key] = key
      
      let dict = mutableDict
      let holder = MCADictionaryHolder()
      holder.randomise(bound)
      benchmark("Access NSDictionary via Swift API") 
         for key in keys 
            let value = holder.objects[key]
            _ = value
         
      
      benchmark("Access NSDictionary via NSDictionary API") 
         let nsDict = holder.objects as NSDictionary
         for key in keys 
            let value = nsDict.object(forKey: key)
            _ = value
         
      
      benchmark("Access NSDictionary via dedicated method") 
         for key in keys 
            let value = holder.item(forKey: key)
            _ = value
         
      
      benchmark("Access to Swift Dictionary via Swift API") 
         for key in keys 
            let value = dict[key]
            _ = value
         
      
   

然后性能测试将显示类似于下图的结果:

Access NSDictionary via Swift API:
.......... 1103.655ms ± 9.358ms (mean ± SD)
Access NSDictionary via NSDictionary API:
.......... 0.263ms ± 0.001ms (mean ± SD)
Access NSDictionary via dedicated method:
.......... 0.335ms ± 0.002ms (mean ± SD)
Access to Swift Dictionary via Swift API:
.......... 0.174ms ± 0.001ms (mean ± SD)

从结果可以看出:

通过 Swift API 访问 Swift Dictionary 可以获得最快的结果。 在 Swift 端通过 NSDictionary API 访问 NSDictionary 有点慢,但可以接受。

因此,无需创建专用方法来对NSDictionary 执行操作,只需将[AnyHashable: Any] 转换为NSDictionary 并执行所需的操作即可。

更新

在某些情况下,通过便捷的方法或属性访问 ObjC 是值得的,以最小化 ObjC Swift 之间的跨界成本。

假设你有一个 ObjC 扩展,如下所示:

@interface NSAppearance (MCA)

-(BOOL)mca_isDark;

@end

-(BOOL)mca_isDark 
   if ([self.name isEqualToString:NSAppearanceNameDarkAqua]) 
      return true;
   
   if ([self.name isEqualToString:NSAppearanceNameVibrantDark]) 
      return true;
   
   if ([self.name isEqualToString:NSAppearanceNameAccessibilityHighContrastDarkAqua]) 
      return true;
   
   if ([self.name isEqualToString:NSAppearanceNameAccessibilityHighContrastVibrantDark]) 
      return true;
   
   return false;


@end

假设您启动了类似于如下所示的性能测试:

class NSStringComparisonTests: LogicTestCase 

   func isDarkUsingSwiftAPI(_ a: NSAppearance) -> Bool 
      switch a.name 
      case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark:
         return true
      default:
         return false
      
   

   func isDarkUsingObjCAPI(_ a: NSAppearance) -> Bool 
      let nsName = a.name.rawValue as NSString
      if nsName.isEqual(to: NSAppearance.Name.darkAqua) 
         return true
      
      if nsName.isEqual(to: NSAppearance.Name.vibrantDark) 
         return true
      
      if nsName.isEqual(to: NSAppearance.Name.accessibilityHighContrastDarkAqua) 
         return true
      
      if nsName.isEqual(to: NSAppearance.Name.accessibilityHighContrastVibrantDark) 
         return true
      
      return false
   

   func test_default() 
      let appearance = NSAppearance.current!
      let numIterations = 1000000
      benchmark("Compare using Swift API", numberOfIterations: numIterations) 
         let value = isDarkUsingSwiftAPI(appearance)
         _ = value
      

      benchmark("Compare using ObjC API", numberOfIterations: numIterations) 
         let value = isDarkUsingObjCAPI(appearance)
         _ = value
      

      benchmark("Compare using ObjC convenience property", numberOfIterations: numIterations) 
         let value = appearance.mca_isDark()
         _ = value
      
   

然后性能测试将显示类似于下图的结果:

Compare using Swift API:
.......... 813.347ms ± 7.560ms (mean ± SD)
Compare using ObjC API:
.......... 534.337ms ± 1.065ms (mean ± SD)
Compare using ObjC convenience property:
.......... 142.729ms ± 0.197ms (mean ± SD)

从结果可以看出,通过便捷方式从 ObjC 世界中获取信息是最快的解决方案。

【讨论】:

以上是关于在 Swift 中访问 object-c NSDictionary 值很慢的主要内容,如果未能解决你的问题,请参考以下文章

Object-C与Swift混编

Object-C与Swift混合开发

Object-C与Swift混合开发

Object-C 中的 Swift 代码:“ClassName”没有可见的@interface 声明选择器“alloc”

iOS开发 - iOS15导航栏适配(Object-C、Swift)

代码分享-Swift版的和Object-C版倒计时