对我来说,这通常是性能。访问对象的 ivar 与使用指向包含此类结构的内存的指针访问 C 中的结构成员一样快。事实上,Objective-C 对象基本上是位于动态分配内存中的 C 结构。这通常与您的代码一样快,甚至手动优化的汇编代码也不会比这更快。
通过 getter/setting 访问 ivar 涉及到一个 Objective-C 方法调用,它比“普通”C 函数调用慢得多(至少 3-4 倍),即使是普通 C 函数调用也已经慢了好几倍访问结构成员。根据您的属性的属性,编译器生成的 setter/getter 实现可能涉及对函数objc_getProperty
/的另一个 C 函数调用objc_setProperty
,因为这些函数必须根据需要retain
/ copy
/autorelease
对象并在必要时进一步执行原子属性的自旋锁定。这很容易变得非常昂贵,我并不是说要慢 50%。
让我们试试这个:
CFAbsoluteTime cft;
unsigned const kRuns = 1000 * 1000 * 1000;
cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
testIVar = i;
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"1: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);
cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
[self setTestIVar:i];
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"2: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);
输出:
1: 23.0 picoseconds/run
2: 98.4 picoseconds/run
这慢了 4.28 倍,这是一个非原子原始 int,几乎是最好的情况;大多数其他情况甚至更糟(尝试原子NSString *
属性!)。因此,如果您可以接受每个 ivar 访问速度比实际速度慢 4-5 倍的事实,那么使用属性就可以了(至少在性能方面),但是,在很多情况下这种性能下降是完全不能接受。
2015-10-20 更新
有人争辩说,这不是现实世界的问题,上面的代码纯粹是合成的,在实际应用程序中你永远不会注意到这一点。好吧,让我们尝试一个真实世界的样本。
下面的代码定义了Account
对象。帐户具有描述其所有者的姓名 ( NSString *
)、性别 ( enum
) 和年龄 ( unsigned
) 以及余额 ( int64_t
) 的属性。一个帐户对象有一个init
方法和一个compare:
方法。方法定义为:compare:
女排男排,姓名按字母顺序,年轻排排老排,余额顺序从低到高。
实际上存在两个帐户类,AccountA
和AccountB
。如果您查看它们的实现,您会注意到它们几乎完全相同,但有一个例外:compare:
方法。AccountA
对象通过方法(getter)访问自己的属性,而对象AccountB
通过ivar访问自己的属性。这真的是唯一的区别!它们都访问另一个对象的属性以通过 getter 进行比较(通过 ivar 访问它并不安全!如果另一个对象是子类并覆盖了 getter 怎么办?)。另请注意,将您自己的属性作为 ivars 访问不会破坏封装(ivars 仍然不公开)。
测试设置非常简单:创建 1 个 Mio 随机帐户,将它们添加到数组并对该数组进行排序。就是这样。当然,有两个数组,一个用于AccountA
对象,一个用于AccountB
对象,并且两个数组都填充了相同的帐户(相同的数据源)。我们计算对数组进行排序所需的时间。
这是我昨天做的几次运行的输出:
runTime 1: 4.827070, 5.002070, 5.014527, 5.019014, 5.123039
runTime 2: 3.835088, 3.804666, 3.792654, 3.796857, 3.871076
如您所见,对AccountB
对象数组进行排序总是比对AccountA
对象数组进行排序要快得多。
谁声称高达 1.32 秒的运行时差异没有任何影响,最好永远不要进行 UI 编程。例如,如果我想更改大表的排序顺序,像这样的时间差异确实会对用户产生巨大的影响(可接受的 UI 和缓慢的 UI 之间的差异)。
同样在这种情况下,示例代码是此处执行的唯一实际工作,但是您的代码多久只是一个复杂发条装置的一个小齿轮?而如果每一个齿轮都像这样拖慢整个过程,那最终对整个发条的速度意味着什么?特别是如果一个工作步骤依赖于另一个工作步骤的输出,这意味着所有的低效率都会总结出来。大多数低效率本身并不是问题,而是它们的总和成为整个过程的问题。并且这样的问题不是分析器容易显示的,因为分析器是关于寻找关键热点的,但是这些低效率本身都不是热点。CPU时间只是平均分布在它们之间,但它们每个都只有很小的一部分,优化它似乎完全浪费时间。这是真的,
而且即使你不考虑CPU时间,因为你认为浪费CPU时间是完全可以接受的,毕竟“它是免费的”,那么由功耗引起的服务器托管成本呢?移动设备的电池运行时间如何?如果您要编写相同的移动应用程序两次(例如,自己的移动网络浏览器),一次是所有类仅通过 getter 访问自己的属性的版本,一次是所有类仅通过 ivars 访问它们的版本,那么不断使用第一个肯定会耗尽电池比使用第二个电池快得多,即使它们在功能上是等效的,并且对于用户来说,第二个电池甚至可能会感觉更快一些。
现在这是您的main.m
文件的代码(代码依赖于启用 ARC 并确保在编译时使用优化以查看完整效果):
#import <Foundation/Foundation.h>
typedef NS_ENUM(int, Gender) {
GenderMale,
GenderFemale
};
@interface AccountA : NSObject
@property (nonatomic) unsigned age;
@property (nonatomic) Gender gender;
@property (nonatomic) int64_t balance;
@property (nonatomic,nonnull,copy) NSString * name;
- (NSComparisonResult)compare:(nonnull AccountA *const)account;
- (nonnull instancetype)initWithName:(nonnull NSString *const)name
age:(const unsigned)age gender:(const Gender)gender
balance:(const int64_t)balance;
@end
@interface AccountB : NSObject
@property (nonatomic) unsigned age;
@property (nonatomic) Gender gender;
@property (nonatomic) int64_t balance;
@property (nonatomic,nonnull,copy) NSString * name;
- (NSComparisonResult)compare:(nonnull AccountB *const)account;
- (nonnull instancetype)initWithName:(nonnull NSString *const)name
age:(const unsigned)age gender:(const Gender)gender
balance:(const int64_t)balance;
@end
static
NSMutableArray * allAcocuntsA;
static
NSMutableArray * allAccountsB;
static
int64_t getRandom ( const uint64_t min, const uint64_t max ) {
assert(min <= max);
uint64_t rnd = arc4random(); // arc4random() returns a 32 bit value only
rnd = (rnd << 32) | arc4random();
rnd = rnd % ((max + 1) - min); // Trim it to range
return (rnd + min); // Lift it up to min value
}
static
void createAccounts ( const NSUInteger ammount ) {
NSArray *const maleNames = @[
@"Noah", @"Liam", @"Mason", @"Jacob", @"William",
@"Ethan", @"Michael", @"Alexander", @"James", @"Daniel"
];
NSArray *const femaleNames = @[
@"Emma", @"Olivia", @"Sophia", @"Isabella", @"Ava",
@"Mia", @"Emily", @"Abigail", @"Madison", @"Charlotte"
];
const NSUInteger nameCount = maleNames.count;
assert(maleNames.count == femaleNames.count); // Better be safe than sorry
allAcocuntsA = [NSMutableArray arrayWithCapacity:ammount];
allAccountsB = [NSMutableArray arrayWithCapacity:ammount];
for (uint64_t i = 0; i < ammount; i++) {
const Gender g = (getRandom(0, 1) == 0 ? GenderMale : GenderFemale);
const unsigned age = (unsigned)getRandom(18, 120);
const int64_t balance = (int64_t)getRandom(0, 200000000) - 100000000;
NSArray *const nameArray = (g == GenderMale ? maleNames : femaleNames);
const NSUInteger nameIndex = (NSUInteger)getRandom(0, nameCount - 1);
NSString *const name = nameArray[nameIndex];
AccountA *const accountA = [[AccountA alloc]
initWithName:name age:age gender:g balance:balance
];
AccountB *const accountB = [[AccountB alloc]
initWithName:name age:age gender:g balance:balance
];
[allAcocuntsA addObject:accountA];
[allAccountsB addObject:accountB];
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
@autoreleasepool {
NSUInteger ammount = 1000000; // 1 Million;
if (argc > 1) {
unsigned long long temp = 0;
if (1 == sscanf(argv[1], "%llu", &temp)) {
// NSUIntegerMax may just be UINT32_MAX!
ammount = (NSUInteger)MIN(temp, NSUIntegerMax);
}
}
createAccounts(ammount);
}
// Sort A and take time
const CFAbsoluteTime startTime1 = CFAbsoluteTimeGetCurrent();
@autoreleasepool {
[allAcocuntsA sortedArrayUsingSelector:@selector(compare:)];
}
const CFAbsoluteTime runTime1 = CFAbsoluteTimeGetCurrent() - startTime1;
// Sort B and take time
const CFAbsoluteTime startTime2 = CFAbsoluteTimeGetCurrent();
@autoreleasepool {
[allAccountsB sortedArrayUsingSelector:@selector(compare:)];
}
const CFAbsoluteTime runTime2 = CFAbsoluteTimeGetCurrent() - startTime2;
NSLog(@"runTime 1: %f", runTime1);
NSLog(@"runTime 2: %f", runTime2);
}
return 0;
}
@implementation AccountA
- (NSComparisonResult)compare:(nonnull AccountA *const)account {
// Sort by gender first! Females prior to males.
if (self.gender != account.gender) {
if (self.gender == GenderFemale) return NSOrderedAscending;
return NSOrderedDescending;
}
// Otherwise sort by name
if (![self.name isEqualToString:account.name]) {
return [self.name compare:account.name];
}
// Otherwise sort by age, young to old
if (self.age != account.age) {
if (self.age < account.age) return NSOrderedAscending;
return NSOrderedDescending;
}
// Last ressort, sort by balance, low to high
if (self.balance != account.balance) {
if (self.balance < account.balance) return NSOrderedAscending;
return NSOrderedDescending;
}
// If we get here, the are really equal!
return NSOrderedSame;
}
- (nonnull instancetype)initWithName:(nonnull NSString *const)name
age:(const unsigned)age gender:(const Gender)gender
balance:(const int64_t)balance
{
self = [super init];
assert(self); // We promissed to never return nil!
_age = age;
_gender = gender;
_balance = balance;
_name = [name copy];
return self;
}
@end
@implementation AccountB
- (NSComparisonResult)compare:(nonnull AccountA *const)account {
// Sort by gender first! Females prior to males.
if (_gender != account.gender) {
if (_gender == GenderFemale) return NSOrderedAscending;
return NSOrderedDescending;
}
// Otherwise sort by name
if (![_name isEqualToString:account.name]) {
return [_name compare:account.name];
}
// Otherwise sort by age, young to old
if (_age != account.age) {
if (_age < account.age) return NSOrderedAscending;
return NSOrderedDescending;
}
// Last ressort, sort by balance, low to high
if (_balance != account.balance) {
if (_balance < account.balance) return NSOrderedAscending;
return NSOrderedDescending;
}
// If we get here, the are really equal!
return NSOrderedSame;
}
- (nonnull instancetype)initWithName:(nonnull NSString *const)name
age:(const unsigned)age gender:(const Gender)gender
balance:(const int64_t)balance
{
self = [super init];
assert(self); // We promissed to never return nil!
_age = age;
_gender = gender;
_balance = balance;
_name = [name copy];
return self;
}
@end