Mac OS X NSArray 枚举性能研究

一天,我在思考 NSArray 枚举方法 (也称迭代方法): Mac OS X 10.6 和 iOS 4 带来了以块(block)组成的美丽新世界,enumerateObjectsUsingBlock: 方法随之而来。我感觉这个方法要慢于快速枚举 (for (object in array) { ... }),因为有总体开销,但我并不能确定。因此我决定做一次性能测评。

都有哪些枚举方法?

总体来说,我们有4种可以使用的枚举方法 (参考 Mike Ash 的 周五常见问题 2010-04-09: Objective-C 的枚举方法对比)。

1、objectAtIndex: enumeration 使用一个 for 循环,递增循环变量,然后用 [myArray objectAtIndex:index] 来访问元素。这是最基本的枚举形式。

NSUInteger count = [myArray count];  



for (NSUInteger index = 0; index < count ; index++) {  



    [self doSomethingWith:[myArray objectAtIndex:index]];  


}  

2、NSEnumerator 外部迭代(external iteration)的形式: [myArray objectEnumerator] 返回一个对象,这个对象有  nextObject 方法。我们可以循环调用这个方法,直到返回 nil 为止。

NSEnumerator *enumerator = [myArray objectEnumerator];  


id object;  



while (object = [enumerator nextObject]) {  



    [self doSomethingWith:object];  


}  

3、NSFastEnumerator The idea behind 快速枚举 的思想是利用 C 数组快速访问 来优化迭代。不仅它理论上比传统的  NSEnumerator 更快,而且 Objective-C 2.0 提供了这种简明的语法:

id object;  



for (object in myArray) {  



    [self doSomethingWith:object];  


} 

4、Block enumeration(块枚举)引入 blocks 后出现的方法,它可以基于块来迭代访问一个数组。它的语法没有快速枚举那么简洁,但它有一个有趣的特性: 并发枚举。如果枚举的顺序并不重要,而且实施的处理可以并发进行,不用锁,这种方法可以在多核系统上带来相当明显的效率提升。详情参考 并发枚举一节

[myArray enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {  


    [self doSomethingWith:object];  


}];  


[myArray enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {  


    [self doSomethingWith:object];  


}]; 

线性枚举

首先,我们讨论一下线性枚举:一个项目接着前一个。

图表

Mac OS X NSArray 枚举性能研究

结论

有一点令人惊讶的是,NSEnumerator甚至比使用objectAtIndex:还慢。这对于Mac OS X 和IOS是一个事实。我猜想这是由于枚举器在每次迭代时都去检查数组是否被修改。自然地,快速枚举保存了每个原始的名字,因此是最快的解决方案。

对于小的数组,block enumeration 比objectAtIndex:稍慢一点,但在有大量元素的数组里,它的性能变得与fast enumeration差不多快。

fast enumeration和NSEnumeration之间的区别在很多地方已经非常明显:对于iPhone 4S,前者花费约0.037秒而后者需要0.140秒。这已经相差了3.7陪。

奇怪的一点

首次在程序中分配 NSArray 和首次用objectEnumerator 获取 enumerator 都需要异常长的时间才能完成。例如,在我 2007 年的 17 寸 MacBook Pro 上分配含一个元素的数组,所需时间的中位数是 415 纳秒。但首次分配的时候会需要 500,000 纳秒,有时甚至要到 1,000,000 纳秒!获取 enumerator 也是如此:尽管中位数只有 673 纳秒,首次获取却要花 500,000 纳秒以上。

我只能猜测其中的原因,但我怀疑延迟加载是罪魁祸首。在实际应用中,你可能不会注意到这一点,因为等到执行你的代码时,Cocoa 或 Cocoa Touch 很可能已经创建过数组了。

并发枚举

如果情况允许,你可以选择用块枚举来并发枚举对象。这意味着计算的工作量可以分散到几个 CPU 内核上。并不是每种枚举过程中的处理都是可并发的,因此只有没用到锁的时候,才能使用并发枚举:要么每一步操作确实是绝对相互独立的,要么有原子性的操作可用 (如 OSAtomicAdd32 之类)。

那么,它相比其他枚举类型有多大优势呢?

图表

Mac OS X NSArray 枚举性能研究

结论

元素不多时,并发枚举是目前最慢的方法。主要原因可能是为了让数组能并发访问而做的准备工作和开启线程(我不知道用的是 GCD 还是“传统的”线程,这不重要;这是我们不需关心的实现细节)。

尽管如此,如果数组足够大,并发枚举突然就成了最快的方法了,正如我们所料。在 iPhone 4S 上枚举 100 万个元素,用并发枚举需要 0.024 秒,但快速枚举需要 0.036 秒。相形之下,还是同一个数组,NSEnumeration 要用 0.139 秒! 这已经是非常大的差距了,足有 5.7 倍之多。

在我的办公室,2011 iMac 24"采用了酷睿i7四核CPU,同时在0.0016秒之内列举了百万项。同一数组快速枚举了0.0044秒和NSEnumeration o.oo93秒。那个因数是5.8,它非常接近于ipone 4S的结果。在这里,我期待一个更大的差异,虽然,在我的2007 MacBook采用了Core2 Duo双核CPU,在这里因数刚好是3.7.当同时枚举的阈值成为有用,在某处以我的测试是10,000和50,000分子之间。用更少的分子元素,去掉正常的块迭代。

分配方式

我也想知道枚举的性能会不会受数组创建方式的影响。我测试了两个不同的方法:

  1. 首先创建一个 C 数组,里面引用了数组元素的对象实例,然后再用 initWithObjects:count: 创建NSArray。

  2. 直接创建 NSMutableArray 并依次用 addObject: 添加对象。

结果是迭代过程的没有区别,但分配过程有所不同:initWithObjects:count: 快一些。数组元素很多时,差距更加显著。这个例子创建了一个元素为 NSNumber 的数组:

NSArray *generateArrayMalloc(NSUInteger numEntries) {  


    id *entries;  


    NSArray *result;  


          


    entries = malloc(sizeof(id) * numEntries);  



    for (NSUInteger i = 0; i < numEntries; i++) {  



        entries[i] = [NSNumber numberWithUnsignedInt:i];  


    }  


      


    result = [NSArray arrayWithObjects:entries count:numEntries];  


      


    free(entries);  



    return result;  



} 

Mac OS X NSArray 枚举性能研究

我是如何来测量的?

你可以从 http://darkdust.net/files/arraytest.m 来下载这个测试应用 看看我是如何来测量的。基本上我就是测量重复迭代一个数组(什么处理也不做)1000次需要多长时间。在图表中,取每个数组尺寸的平均值。这个应用的编译选项是关闭优化(-O0)。对于 iOS,我是在一个 iPhone 4S 上进行的测试。对 MAC OS X,我用我家里2007年产的 MacBook Pro 17”和我办公室2011年产的 iMac 24”来测试。MAC OS X的图表显示的是iMac上的结果,在MacBook Pro上的图表看起来与此相似,只是更慢一些。

mac

相关推荐