Cocoa编程的一个通常的任务是要去循环遍历一个对象的集合 (例如,一个 NSArray, NSSet 或者是 NSDictionary). 这个看似简单的问题有广泛数量的解决方案,它们中的许多不乏有对性能方面问题的细微考虑.
首先,是一个免责声明: 相比其它问题而言,一个 Objective-C 方法原始的速度是你在编程时最后才需要考虑的问题之一 – 区别就在于这个问题够不上去同其它更加需要重点考虑的问题进行比较,比如说代码的清晰度和可读性.
但速度的次要性并不妨碍我们去理解它. 你应该经常去了解一下性能方面的考虑将如何对你正在编写的代码产生影响,一边在极少数发生问题的情况下,你会知道如何下手.
还有,在循环的场景中,大多数时候不管是从可读性或者是清晰度考虑,你选择哪种技术都没什么关系的, 所以你还不如选择速度最快的那一种. 没有必要选择编码速度比要求更慢的。
考虑到这一点,就有了如下的选择:
for (NSUInteger i = 0; i < [array count]; i++){ id object = array[i]; …} |
这是循环遍历一个数组的一个简单熟悉的方式; 从性能方面考虑它也相当的差劲. 这段代码最大的问题就是循环每进行一次我们都会调用数组的计数方法. 数组的总数是不会改变的,因此每次都去调用一下这种做法是多余的. 像这种代码一般C编译器一般都会优化掉, 但是 Objective-C 的动态语言特性意味着对这个方法的调用不会被自动优化掉. 因此,为了提升性能,值得我们在循环开始之前,将这个总数存到一个变量中,像这样:
NSUInteger count = [array count];for (NSUInteger i = 0; i < count; i++){ id object = array[i]; …} |
NSEnumerator 是循环遍历集合的一种可选方式. 所有的集合都已一个或者更多个枚举方法,每次它们被调用的时候都会返回一个NSEnumerator实体. 一个给定的 NSEnumerator 会包含一个指向集合中第一个对象的指针, 并且会有一个 nextObject 方法返回当前的对象并对指针进行增长. 你可以重复调用它直到它返回nil,这表明已经到了集合的末尾了:
id obj = nil;NSEnumerator *enumerator = [array objectEnumerator];while ((obj = [enumerator nextObject]));{ … } |
NSEnumerator 的性能可以媲美原生的for循环, 但它更加实用,因为它对索引的概念进行了抽象,这意味着它应用在结构化数据上,比如链表,或者甚至是无穷序列和数据流,这些结构中的数据条数未知或者并没有被定义.
快速枚举是在 Objective-C 2.0 中作为传统的NSEnumerator的更便利(并且明显更快速) 的替代方法而引入的. 它并没有使得枚举类过时因为其仍然被应用于注入反向枚举, 或者是当你需要对集合进行变更操作 (之后会更多地提到) 这些场景中.
快速枚举添加了一个看起来像下面这样子的新的枚举方法:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len; |
如果你正在想着“那看起来并不怎么舒服啊!”, 我不会怪你的. 但是新的方法顺便带来了一种新的循环语法, for…in 循环. 这是在幕后使用了新的枚举方法, 并且重要的是在语法和性能上都比使用传统的for循环或者 NSEnumerator 方法都更省心了:
for (id object in array){ …} |
那么这些方法叠加起来会如何呢, 性能会更加的好么? 这里有一个简单的基准测试命令行应用,比较了使用多种不同方法枚举一个数据的性能. 我们已经在 ARC 关闭的情况下运行了它,以排除任何干扰最终结果的隐藏在幕后的保留或者排除处理. 由于是运行在一个很快的 Mac 机上面, 所有这些方法运行极快以至于我们实际上不得不使用一个存有10,000,000 (一千万) 对象的数组来测量结果. 如果你决定在一个 iPhone 进行测试, 最明智的做法是使用一个小得多的数量!
为了编译这段代码:
把代码保存在一个文件中,命名为 benchmark.m
在终端中编译应用程序:
clang -framework Foundation benchmark.m -o benchmark
运行程序: ./benchmark
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]){ @autoreleasepool { static const NSUInteger arrayItems = 10000000; NSMutableArray *array = [NSMutableArray arrayWithCapacity:arrayItems]; for (int i = 0; i < arrayItems; i++) [array addObject:@(i)]; array = [array copy]; CFTimeInterval start = CFAbsoluteTimeGetCurrent(); // Naive for loop for (NSUInteger i = 0; i < [array count]; i++) { id object = array[i]; } CFTimeInterval forLoop = CFAbsoluteTimeGetCurrent(); NSLog(@"For loop: %g", forLoop - start); // Optimized for loop NSUInteger count = [array count]; for (NSUInteger i = 0; i < count; i++) { id object = array[i]; } CFTimeInterval forLoopWithCountVar = CFAbsoluteTimeGetCurrent(); NSLog(@"Optimized for loop: %g", forLoopWithCountVar - forLoop); // NSEnumerator id obj = nil; NSEnumerator *enumerator = [array objectEnumerator]; while ((obj = [enumerator nextObject])) { } CFTimeInterval enumeratorLoop = CFAbsoluteTimeGetCurrent(); NSLog(@"Enumerator: %g", enumeratorLoop - forLoopWithCountVar); // Fast enumeration for (id object in array) { } CFTimeInterval forInLoop = CFAbsoluteTimeGetCurrent(); NSLog(@"For…in loop: %g", forInLoop - enumeratorLoop); // Block enumeration [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { }]; CFTimeInterval enumerationBlock = CFAbsoluteTimeGetCurrent(); NSLog(@"Enumeration block: %g", enumerationBlock - forInLoop); // Concurrent enumeration [array enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { }]; CFTimeInterval concurrentEnumerationBlock = CFAbsoluteTimeGetCurrent(); NSLog(@"Concurrent enumeration block: %g", concurrentEnumerationBlock - enumerationBlock); } return 0;} |
下面展示出了结果:
$ For loop: 0.119066 $ Optimized for loop: 0.092441 $ Enumerator: 0.123687 $ For…in loop: 0.049296 $ Enumeration block: 0.295039 $ Concurrent enumeration block: 0.199684
忽略掉时间的具体长短. 我们感兴趣的是它们同其它方法比较的相对大小. 如果我们按顺序排列它们,快的放前面,我会得到了下面的结果:
For…in循环 – 最快.
对for循环的优化 – 比 for…in 慢两倍.
没有优化的for循环 – 比 for…in 慢2.5倍.
Enumerator – 大约同没有优化的循环相同.
并发的枚举块 – 比 for…in 大约慢6倍.
枚举块 – 比 for…in 几乎慢6倍.
For…in 是胜出者. 显然他们将其称为快速枚举是有原因的! 并发枚举看起来是比单线程的快一点点, 但是你没必要对其做更多的解读: 我们这里是在枚举一个非常非常大型的对象数组,而对于小一些的数据并发执行的开销远多于其带来的好处.
并发执行的主要是在当你的循环需要大量的执行时间时有优势. 如果你在自己的循环中有许多东西要运行,那就考虑试下并行枚举,在你不关心枚举顺序的前提下 (但是请用行动的去权衡一下它是否变得更快乐,不要空手去揣度).
那么其它的结合类型怎么样呢, 比如 NSSet 和 NSDictionary? NSSet 是无序的, 因此没有按索引去取对象的概念.我们也可以进行一下基准测试:
$ Enumerator: 0.421863 $ For…in loop: 0.095401 $ Enumeration block: 0.302784 $ Concurrent enumeration block: 0.390825
结果同 NSArray 一致; for…in 再一次胜出了. NSDictionary怎么样了? NSDictionary 有一点不同因为我们同时又一个键和值对象需要迭代. 在一个字典中单独迭代键或者值是可以的, 但典型的情况下我们两者都需要. 这里我们有一段适配于操作NSDictionary的基准测试代码:
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]){ @autoreleasepool { static const NSUInteger dictItems = 10000; NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:dictItems]; for (int i = 0; i < dictItems; i++) dictionary[@(i)] = @(i); dictionary = [dictionary copy]; CFTimeInterval start = CFAbsoluteTimeGetCurrent(); // Naive for loop for (NSUInteger i = 0; i < [dictionary count]; i++) { id key = [dictionary allKeys][i]; id object = dictionary[key]; } CFTimeInterval forLoop = CFAbsoluteTimeGetCurrent(); NSLog(@"For loop: %g", forLoop - start); // Optimized for loop NSUInteger count = [dictionary count]; NSArray *keys = [dictionary allKeys]; for (NSUInteger i = 0; i < count; i++) { id key = keys[i]; id object = dictionary[key]; } CFTimeInterval forLoopWithCountVar = CFAbsoluteTimeGetCurrent(); NSLog(@"Optimized for loop: %g", forLoopWithCountVar - forLoop); // NSEnumerator id key = nil; NSEnumerator *enumerator = [dictionary keyEnumerator]; while ((key = [enumerator nextObject])) { id object = dictionary[key]; } CFTimeInterval enumeratorLoop = CFAbsoluteTimeGetCurrent(); NSLog(@"Enumerator: %g", enumeratorLoop - forLoopWithCountVar); // Fast enumeration for (id key in dictionary) { id object = dictionary[key]; } CFTimeInterval forInLoop = CFAbsoluteTimeGetCurrent(); NSLog(@"For…in loop: %g", forInLoop - enumeratorLoop); // Block enumeration [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { }]; CFTimeInterval enumerationBlock = CFAbsoluteTimeGetCurrent(); NSLog(@"Enumeration block: %g", enumerationBlock - forInLoop); // Concurrent enumeration [dictionary enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(id key, id obj, BOOL *stop) { }]; CFTimeInterval concurrentEnumerationBlock = CFAbsoluteTimeGetCurrent(); NSLog(@"Concurrent enumeration block: %g", concurrentEnumerationBlock - enumerationBlock); } return 0;} |
NSDictionary 填充起来比 NSArray 或者 NSSet 慢得多, 因此我们把数据条数减少到了10,000 (一万) 以避免机器锁住. 因而你应该忽略结果怎么会比那些 NSArray 低那么多,因为我们使用的是更少对象的 1000 次循环:
$ For loop: 2.25899 $ Optimized for loop: 0.00273103 $ Enumerator: 0.00496799 $ For…in loop: 0.001041 $ Enumeration block: 0.000607967 $ Concurrent enumeration block: 0.000748038
没有优化过的循环再这里慢得很壮观,因为每一次我们都复制了键数组. 通过把键数组和总数存到变量中,我们获得了更快的速度. 查找对象的消耗现在主宰了其它的因素,因此使用一个for循环, NSEnumerator 或者for…in 差别很小. 但是对于枚举块方法而言,它在一个方法中把键和值都返回了,所以现在变成了最快的选择。
基于我们所见,如果所有其它的因素都一样的话,在循环遍历数组时你应该尝试去使用for...in循环, 而遍历字典时,则应该选择枚举块. 也有一些场景下这样的做法并不可能行得通,比如我们需要回头来进行枚举,或者当我们在遍历时想要变更集合的情况.
为了回过头来枚举一个数据,我们可以调用reverseObjectEnumerator方法来获得一个NSEnumerator 以从尾至头遍历数组. NSEnumerator, 就像是 NSArray 它自己, 支持快速的枚举协议. 那就意味着我们仍然可以在这种方式下使用 for…in, 而无速度和简洁方面的损失:
for (id object in [array reverseObjectEnumerator]) { … } |
(除非你异想天开, NSSet 或者 NSDictionary 是没有等效的方法的, 而反向枚举一个 NSSet 或者NSDictionary无论如何都没啥意义, 因为键是无序的.)
如果你想使用枚举块的话, NSEnumerationReverse你可以试试, 像这样:
[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { … }]; |
评论删除后,数据将无法恢复
评论(10)
引用来自“afpro”的评论
Object-C的语法简直就是XXXXX
引用来自“sucker”的评论
这种编译器自己做的事,非要人工去优化啊,每天的破事都忙不完了,人艰不拆啊
引用来自“sucker”的评论
这种编译器自己做的事,非要人工去优化啊,每天的破事都忙不完了,人艰不拆啊
引用来自“LeoXu”的评论
引用来自“isaced”的评论
@LeoXu 大哥,你看看这篇文章标题翻译对了吗?
引用来自“isaced”的评论
@LeoXu 大哥,你看看这篇文章标题翻译对了吗?