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

Accessing object-c NSDictionary values in Swift is slow

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

我有一个objc类型Car

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

然后在 swift 中我枚举了大量 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

问题似乎是在您的第一个版本中,在循环的每次迭代中,整个字典都转换为 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 方法是一个很好的解决方案。

问题的一部分是桥接 NSDictionary<NSString *, Part *>[String: Part] 涉及对字典的所有键和值的运行时检查。这是必需的,因为 NSDictionary 的 Objective-C 泛型参数不能保证字典不会与 keys/values 不兼容(例如 Objective-C 代码可以添加非字符串键或字典中的非部分值)。对于大量词典,这可能会变得非常耗时。

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

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

假设你有一个 ObjC class 如下所示:

@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 { "\([=11=])" }
      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 字典给出了最快的结果。
  • 在 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 世界获取信息是最快的解决方案。