【问题标题】:Accessing object-c NSDictionary values in Swift is slow在 Swift 中访问 object-c NSDictionary 值很慢
【发布时间】: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

【问题讨论】:

标签: objective-c swift nsdictionary


【解决方案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 世界中获取信息是最快的解决方案。

    【讨论】:

      猜你喜欢
      • 2017-02-28
      • 1970-01-01
      • 2020-06-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-01-25
      相关资源
      最近更新 更多