访问 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 世界获取信息是最快的解决方案。
我在 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 世界获取信息是最快的解决方案。