NSArray 与 C Array 性能比较

Posted

技术标签:

【中文标题】NSArray 与 C Array 性能比较【英文标题】:NSArray vs C Array performance comparison 【发布时间】:2013-07-10 09:31:44 【问题描述】:

我最近一直在运行一个小型研究项目,研究与 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%)

最快的方法似乎是使用“*(carray + idx)”直接访问C Array中的项目,但最令人费解的是,将C Array中的指针分配给目标c变量“id object = *(carry + idx)" 会造成巨大的性能损失。

我最初认为可能是 arc 使用引用计数做一些事情,因为变量很强大,所以此时我将其更改为弱,期望性能增加“__weak id object = *(carry + idx)”。令我惊讶的是,它实际上慢了很多。

根据序列结果,随机访问结果非常好,因此没有任何意外。

因此有很多问题:

    为什么分配给变量需要这么长时间? 为什么分配给弱变量需要更长的时间? (也许这里发生了一些我不明白的事情) 考虑到上述情况,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];

NSTestObject 是这样定义的:

@interface NSTestObject : NSObject

- (void)doSomething;

@end

@implementation NSTestObject

    float f;


- (void)doSomething

    f++;

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

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;


最后这就是我执行实际测试的方式:

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];

【问题讨论】:

必读:Ridiculous Fish: Array 【参考方案1】:

为什么赋值给一个变量需要这么长时间?

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

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

回想一下,ARC 承诺在最后一个强引用消失时清除您的弱引用。这就是为什么弱引用更昂贵的原因:为了将nil 取出__weak id,ARC 将id 的地址注册到运行时以获取对象被释放的通知。这种注册需要写入一个哈希表——比仅仅保留和释放要慢得多。

考虑到上述情况,Apple 是如何让标准快速枚举表现如此出色的?

快速枚举使用直接支持NSArray 的数组块。本质上,它们抓取大约 30 个元素的块,并将对它的访问视为纯 C 数组。然后他们抓取下一个块,像它是一个 C 数组一样对其进行迭代,依此类推。有一些小的开销,但它是按块计算的,而不是按元素计算的,因此您会获得相当可观的性能。

【讨论】:

感谢您的回复,我实际上尝试在循环中抓取最多 10 个元素的块并分配它们,但它对性能的影响几乎为零,因为大部分循环时间 (~80%) 都花费了关于变量赋值。我现在能想到的唯一解释是,快速枚举会保留每个对象块,并在运行下一个块时以某种方式将它们全部释放到后台线程上。但我没有办法支持这一点。 @Andy 我认为快速枚举根本不会保留和释放对象:我认为他们使用__unsafe_unretained,因为该对象已经归数组所有。 您先生是我的英雄。将 __weak 快速更改为 __unsafe_unretained,性能现在为 8 毫秒,比 NSArray 更快。我没有意识到使用 __unsafe_unretained 会有这样的性能优势,但是正如您正确地说的那样,当它从范围中删除时,__weak 引用必须将值设置为 nil,所以它确实有意义。非常感谢您的帮助!【参考方案2】:

2) 因为你不可能进行优化。静态内存中包含的弱变量,访问静态的时间比动态的长

【讨论】:

以上是关于NSArray 与 C Array 性能比较的主要内容,如果未能解决你的问题,请参考以下文章

从Objective C中的NSArray转换时如何比较String?

如何在 Swift 中比较两个可选的 NSArray

ios开发之--比较两个数组里面的值是否相同

Swift中实现Array数组和NSArray数组的相互转换与遍历

Array 与 NSArray 的不同点对比表

php array_push 与 $arr[]=$value 性能比较