NSArray与C阵列性能比较

And*_*ndy 12 objective-c nsarray ios automatic-ref-counting

我最近一直在运行一个关于与C阵列相关的NSArray顺序或随机访问性能的研究项目.大多数测试用例都出现了,因为我预计会有一些测试用例不起作用,我希望有人可以解释原因.

从根本上说,测试包括用50k对象填充C数组,迭代每个对象并调用一个方法(内部只增加对象中的一个浮点数),测试的第二部分涉及创建一个循环,完成50k次迭代但访问一个数组中的随机对象.基本上它很简单.

为了进行比较,我正在用C数组初始化NSArray.然后通过传递给跟踪执行块所花费时间的方法的块来运行每个测试.我正在使用的代码包含在下面,但我想首先介绍我的结果和查询.

这些测试在iPhone 4上运行并包装在dispatch_after中,以缓解因启动应用程序而导致的任何剩余线程或非原子操作.单次运行的结果如下,每次运行基本相同,只有很小的变化:

===SEQUENCE===
NSARRAY FAST ENUMERATION: 12ms
NSARRAY FAST ENUMERATION WEAK: 186ms
NSARRAY BLOCK ENUMERATION: 31ms (258.3%)
C ARRAY DIRECT: 7ms (58.3%)
C ARRAY VARIABLE ASSIGN: 33ms (275.0%)
C ARRAY VARIABLE ASSIGN WEAK: 200ms (1666.7%)

===RANDOM===
NSARRAY RANDOM: 102ms (850.0%) *Relative to fast enumeration
C ARRAY DIRECT RANDOM: 39ms (38.2%) *Relative to NSArray Random
C ARRAY VARIABLE ASSIGN RANDOM: 82ms (80.4%)
Run Code Online (Sandbox Code Playgroud)

最快的方法似乎是使用"*(carray + idx)"直接访问C数组中的项目,但最令人费解的是将指针从C数组分配给目标c变量"id object =*(carry) + idx)"导致巨大的性能损失.

我认为最初可能是做参考计数的电弧,因为变量很强,所以此时我把它改为弱,期望性能增加"__weak id object =*(carry + idx)".令我惊讶的是它实际上要慢得多.

随机访问结果非常好,我根据序列结果预期,所以幸运的是没有惊喜.

因此,有许多问题:

  1. 为什么分配给变量需要这么长时间?
  2. 为什么分配给弱变量需要更长时间?(也许这里有一些我不明白的事情)
  3. 考虑到上述问题,Apple如何获得标准的快速枚举才能表现如此出色?

为了完整起见,这里是代码.所以我按如下方式创建数组:

__block id __strong *cArrayData = (id __strong *)malloc(sizeof(id) * ITEM_COUNT);

for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) {
    NSTestObject *object = [[NSTestObject alloc] init];
    cArrayData[idx] = object;
}

__block NSArray *arrayData = [NSArray arrayWithObjects:cArrayData count:ITEM_COUNT];
Run Code Online (Sandbox Code Playgroud)

NSTestObject的定义如下:

@interface NSTestObject : NSObject

- (void)doSomething;

@end

@implementation NSTestObject
{
    float f;
}

- (void)doSomething
{
    f++;
}
Run Code Online (Sandbox Code Playgroud)

以及用于分析代码的方法:

int machTimeToMS(uint64_t machTime)
{
    const int64_t kOneMillion = 1000 * 1000;
    static mach_timebase_info_data_t s_timebase_info;

    if (s_timebase_info.denom == 0) {
        (void) mach_timebase_info(&s_timebase_info);
    }
    return (int)((machTime * s_timebase_info.numer) / (kOneMillion * s_timebase_info.denom));
}

- (int)profile:(dispatch_block_t)call name:(NSString *)name benchmark:(int)benchmark
{

    uint64_t startTime, stopTime;
    startTime = mach_absolute_time();

    call();

    stopTime = mach_absolute_time();

    int duration = machTimeToMS(stopTime - startTime);

    if (benchmark > 0) {
        NSLog(@"%@: %i (%0.1f%%)", name, duration, ((float)duration / (float)benchmark) * 100.0f);
    } else {
        NSLog(@"%@: %i", name, duration);
    }

    return duration;

}
Run Code Online (Sandbox Code Playgroud)

最后,这就是我正在进行的实际测试:

int benchmark = [self profile:^ {
    for (NSTestObject *view in arrayData) {
        [view doSomething];
    }
} name:@"NSARRAY FAST ENUMERATION" benchmark:0];

[self profile:^ {
    for (NSTestObject __weak *view in arrayData) {
        [view doSomething];
    }
} name:@"NSARRAY FAST ENUMERATION WEAK" benchmark:0];

[self profile:^ {
    [arrayData enumerateObjectsUsingBlock:^(NSTestObject *view, NSUInteger idx, BOOL *stop) {
        [view doSomething];
    }];
} name:@"NSARRAY BLOCK ENUMERATION" benchmark:benchmark];

[self profile:^ {
    for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) {
        [*(cArrayData + idx) doSomething];
    }
} name:@"C ARRAY DIRECT" benchmark:benchmark];

[self profile:^ {
    id object = nil;
    NSUInteger idx = 0;
    while (idx < ITEM_COUNT) {
        object = (id)*(cArrayData + idx);
        [object doSomething];
        object = nil;
        idx++;
    }
} name:@"C ARRAY VARIABLE ASSIGN" benchmark:benchmark];

[self profile:^ {
    __weak id object = nil;
    NSUInteger idx = 0;
    while (idx < ITEM_COUNT) {
        object = (id)*(cArrayData + idx);
        [object doSomething];
        object = nil;
        idx++;
    }
} name:@"C ARRAY VARIABLE ASSIGN WEAK" benchmark:benchmark];

NSLog(@"\n===RANDOM===\n");

benchmark = [self profile:^ {
    id object = nil;
    for (NSUInteger idx = 0; idx < ITEM_COUNT; idx ++) {
        object = arrayData[arc4random()%ITEM_COUNT];
        [object doSomething];
    }
} name:@"NSARRAY RANDOM" benchmark:benchmark];

[self profile:^ {
    NSUInteger idx = 1;
    while (idx < ITEM_COUNT) {
        [*(cArrayData + arc4random()%ITEM_COUNT) doSomething];
        idx++;
    }
} name:@"C ARRAY DIRECT RANDOM" benchmark:benchmark];

[self profile:^ {
    id object = nil;
    NSUInteger idx = 0;
    while (idx < ITEM_COUNT) {
        object = (id)*(cArrayData + arc4random()%ITEM_COUNT);
        [object doSomething];
        idx++;
    }
} name:@"C ARRAY VARIABLE ASSIGN RANDOM" benchmark:benchmark];
Run Code Online (Sandbox Code Playgroud)

das*_*ght 6

为什么分配给变量需要这么长时间?

您的猜测是正确的:retain当您分配,release重新分配或id超出范围时,ARC会调用.

为什么分配给弱变量需要更长时间?(也许这里有一些我不明白的事情)

回想一下,当最后一个强引用消失时,ARC承诺清除你的弱引用.这就是为什么弱引用更昂贵的原因:为了nil输出__weak idARC寄存器id的地址和运行时来获取被释放对象的通知.这种注册需要写入哈希表 - 远比保留和释放慢得多.

考虑到上述问题,Apple如何获得标准的快速枚举才能表现如此出色?

快速枚举使用NSArray直接支持的数组块.从本质上讲,它们可以获取大约30个元素的块,并将其作为普通C数组访问.然后他们抓住下一个块,迭代它就好像它是一个C数组,依此类推.有一些小的开销,但它是每个块,而不是每个元素,所以你得到一个非常令人印象深刻的性能.

  • @Andy我不认为快速枚举会保留和释放对象:我认为它们使用`__unsafe_unretained`,因为该对象已经被数组拥有. (3认同)