Cocoa编程的一个通常的任务是要去循环遍历一个对象的集合  (例如,一个 NSArray, NSSet 或者是 NSDictionary). 这个看似简单的问题有广泛数量的解决方案,它们中的许多不乏有对性能方面问题的细微考虑.

对于速度的追求

首先,是一个免责声明: 相比其它问题而言,一个 Objective-C 方法原始的速度是你在编程时最后才需要考虑的问题之一 – 区别就在于这个问题够不上去同其它更加需要重点考虑的问题进行比较,比如说代码的清晰度和可读性.

但速度的次要性并不妨碍我们去理解它. 你应该经常去了解一下性能方面的考虑将如何对你正在编写的代码产生影响,一边在极少数发生问题的情况下,你会知道如何下手.

还有,在循环的场景中,大多数时候不管是从可读性或者是清晰度考虑,你选择哪种技术都没什么关系的, 所以你还不如选择速度最快的那一种. 没有必要选择编码速度比要求更慢的。

考虑到这一点,就有了如下的选择:

经典的循环方式

?
1
2
3
for (NSUInteger i = 0; i < [array count]; i++){
  id object = array[i];
  …}

这是循环遍历一个数组的一个简单熟悉的方式; 从性能方面考虑它也相当的差劲. 这段代码最大的问题就是循环每进行一次我们都会调用数组的计数方法. 数组的总数是不会改变的,因此每次都去调用一下这种做法是多余的. 像这种代码一般C编译器一般都会优化掉, 但是 Objective-C 的动态语言特性意味着对这个方法的调用不会被自动优化掉. 因此,为了提升性能,值得我们在循环开始之前,将这个总数存到一个变量中,像这样:

?
1
2
3
NSUInteger count = [array count];for (NSUInteger i = 0; i < count; i++){
  id object = array[i];
  …}

NSEnumerator

NSEnumerator 是循环遍历集合的一种可选方式. 所有的集合都已一个或者更多个枚举方法,每次它们被调用的时候都会返回一个NSEnumerator实体. 一个给定的 NSEnumerator 会包含一个指向集合中第一个对象的指针, 并且会有一个 nextObject 方法返回当前的对象并对指针进行增长. 你可以重复调用它直到它返回nil,这表明已经到了集合的末尾了:

?
1
2
3
id obj = nil;NSEnumerator *enumerator = [array objectEnumerator];while ((obj = [enumerator nextObject]));{
  …          
}

NSEnumerator 的性能可以媲美原生的for循环, 但它更加实用,因为它对索引的概念进行了抽象,这意味着它应用在结构化数据上,比如链表,或者甚至是无穷序列和数据流,这些结构中的数据条数未知或者并没有被定义.

快速枚举

快速枚举是在 Objective-C 2.0 中作为传统的NSEnumerator的更便利(并且明显更快速) 的替代方法而引入的. 它并没有使得枚举类过时因为其仍然被应用于注入反向枚举, 或者是当你需要对集合进行变更操作 (之后会更多地提到) 这些场景中.

快速枚举添加了一个看起来像下面这样子的新的枚举方法:

?
1
2
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state 
   objects:(id *)stackbuf count:(NSUInteger)len;

如果你正在想着“那看起来并不怎么舒服啊!”, 我不会怪你的. 但是新的方法顺便带来了一种新的循环语法, for…in 循环. 这是在幕后使用了新的枚举方法, 并且重要的是在语法和性能上都比使用传统的for循环或者 NSEnumerator 方法都更省心了:

?
1
2
for (id object in array){
  …}

 

枚举块

随着块的诞生,Apple加入第四个基于块语法的枚举机制. 这无疑比快速枚举更加的少见, 但是有一个优势就是对象和索引都会返回, 而其他的枚举方法只会返回对象.

枚举块的另外一个关键特性就是可选择型的并发枚举 (在几个并发的线程中枚举对象). 这不是经常有用,取决于你在自己的循环中具体要做些什么, 但是在你正有许多工作要做,并且你并不怎么关心枚举顺序的场景下,它在多核处理器上可能会产生显著的性能提高 (现在所有的 Mac和iOS设备都已经有了多核处理器).

基准测试

那么这些方法叠加起来会如何呢, 性能会更加的好么? 这里有一个简单的基准测试命令行应用,比较了使用多种不同方法枚举一个数据的性能. 我们已经在 ARC 关闭的情况下运行了它,以排除任何干扰最终结果的隐藏在幕后的保留或者排除处理. 由于是运行在一个很快的 Mac 机上面, 所有这些方法运行极快以至于我们实际上不得不使用一个存有10,000,000 (一千万) 对象的数组来测量结果. 如果你决定在一个 iPhone 进行测试, 最明智的做法是使用一个小得多的数量!

为了编译这段代码:

  • 把代码保存在一个文件中,命名为 benchmark.m

  • 在终端中编译应用程序:
    clang -framework Foundation benchmark.m -o benchmark

  • 运行程序: ./benchmark

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#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;}

下面展示出了结果:

?
1
2
3
4
5
6
$ 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

leoxu
翻译于 1年前

0人顶

顶 翻译的不错哦!

忽略掉时间的具体长短. 我们感兴趣的是它们同其它方法比较的相对大小. 如果我们按顺序排列它们,快的放前面,我会得到了下面的结果:

  1. For…in循环 – 最快.

  2. 对for循环的优化 – 比 for…in 慢两倍.

  3. 没有优化的for循环 – 比 for…in 慢2.5倍.

  4. Enumerator – 大约同没有优化的循环相同.

  5. 并发的枚举块 – 比 for…in 大约慢6倍.

  6. 枚举块 – 比 for…in 几乎慢6倍.

For…in 是胜出者. 显然他们将其称为快速枚举是有原因的! 并发枚举看起来是比单线程的快一点点, 但是你没必要对其做更多的解读: 我们这里是在枚举一个非常非常大型的对象数组,而对于小一些的数据并发执行的开销远多于其带来的好处.

并发执行的主要是在当你的循环需要大量的执行时间时有优势. 如果你在自己的循环中有许多东西要运行,那就考虑试下并行枚举,在你不关心枚举顺序的前提下 (但是请用行动的去权衡一下它是否变得更快乐,不要空手去揣度).

其它集合类型Other Collection Types

那么其它的结合类型怎么样呢, 比如 NSSet 和 NSDictionary? NSSet 是无序的, 因此没有按索引去取对象的概念.我们也可以进行一下基准测试:

?
1
2
3
4
$ Enumerator: 0.421863
$ For…in loop: 0.095401
$ Enumeration block: 0.302784
$ Concurrent enumeration block: 0.390825

结果同 NSArray 一致; for…in 再一次胜出了.  NSDictionary怎么样了? NSDictionary 有一点不同因为我们同时又一个键和值对象需要迭代. 在一个字典中单独迭代键或者值是可以的, 但典型的情况下我们两者都需要. 这里我们有一段适配于操作NSDictionary的基准测试代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#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 次循环:

?
1
2
3
4
5
6
$ 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, 而无速度和简洁方面的损失:

?
1
2
3
  for (id object in [array reverseObjectEnumerator]) 
  {
    …  }

(除非你异想天开, NSSet 或者 NSDictionary 是没有等效的方法的, 而反向枚举一个 NSSet 或者NSDictionary无论如何都没啥意义, 因为键是无序的.)

如果你想使用枚举块的话, NSEnumerationReverse你可以试试, 像这样:

?
1
2
  [array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    …  }];

变更Mutation

应用同样的循环技术到变更中的集合上是可能的; 其性能也大致相同. 然而当你尝试在循环数组或者字典的时候修改它们,你可能经常会面临这样的异常:

'*** Collection XYZ was mutated while being enumerated.'

就像我们优化了的for循环, 所有这些循环技术的性能取决于事先把数据总数存下来,这意味着如果你开始在循环中间加入或者去掉一个数据时,这个数据就不正确了. 但是在循环进行中加入,替换或者移除一条数据时经常想要做的事情. 那么什么才是这个问题的解决之道呢?

我们经典的for循环可以工作得很好,因为它不依赖于驻留的总数常量; 我们只需要记得,如果我们添加或者移除了一条数据,就要增加或者减小索引. 但我们已经了解到for循环并不是一种速度快的解决方案. 我们优化过的for循环则是一个合理的选择, 只要我们记得按需递增或者递减技术变量,还有索引.

我们仍然可以使用for…in, 但前提是我们首先创建了一个数组的拷贝. 这会起作用的,例如:

  for (id object in [array copy]) {// Do something that modifies the array, e.g. [array removeObject:object];}

如果我们对不同的技术进行基准测试(必要时把复制数组的开销算在内,以便我们可以对原来数组内的数据进行变更), 我们发现复制抵消了 for…in 循环之前所拥有的好处:

$ For loop: 0.111422
$ Optimized for loop: 0.08967
$ Enumerator: 0.313182
$ For…in loop: 0.203722
$ Enumeration block: 0.436741
$ Concurrent enumeration block: 0.388509

在我们遍历一个数组时修改这个数组最快的计数,似乎是需要使用一个优化了的for循环的.

对于一个 NSDictionary, 我们不需要为了使用NSEnumerator 或者快速枚举而复制整个字典; 我们可以只去使用allKeys方法获取到所有键的一个副本. 这都将能很好的运作起来:

  // NSEnumeratorid key = nil;  NSEnumerator *enumerator = [[items allKeys] objectEnumerator];  while ((key = [enumerator nextObject])){id object = items[key];    // Do something that modifies the value, e.g. dictionary[key] = newObject;}   // Fast enumerationfor (id key in [dictionary allkeys]) {id object = items[key];    // Do something that modifies the value, e.g. dictionary[key] = newObject;}

然而同样的技术在使用enumerateKeysAndObjectsUsingBlock方法时并不能起作用. 如果我们循环遍历一个字典进行基准测试, 按照需要对键或者对字典整体创建备份,我们得到了下面的结果:

$ For loop: 2.24597
$ Optimized for loop: 0.00282001
$ Enumerator: 0.00508499
$ For…in loop: 0.000990987
$ Enumeration block: 0.00144804
$ Concurrent enumeration block: 0.00166804

这里我们可以看到 for…in 循环是最快的一个. 那是因为在for...in循环中根据键取对象的开销现在已经被在调用枚举块方法之前复制字典的开销盖过去了.

当枚举一个NSArray的时候:

  • 使用 for (id object in array) 如果是顺序枚举

  • 使用 for (id object in [array reverseObjectEnumerator]) 如果是倒序枚举

  • 使用 for (NSInteger i = 0; i < count; i++) 如果你需要知道它的索引值,或者需要改变数组

  • 尝试 [array enumerateObjectsWithOptions:usingBlock:] 如果你的代码受益于并行执行

当枚举一个NSSet的时候:

  • 使用  for (id object in set) 大多数时候

  • 使用 for (id object in [set copy]) 如果你需要修改集合(但是会很慢)

  • 尝试 [array enumerateObjectsWithOptions:usingBlock:] 如果你的代码受益于并行执行

当枚举一个NSDictionary的时候:

  • 使用  for (id object in set) 大多数时候

  • 使用 for (id object in [set copy]) 如果你需要修改词典

  • 尝试 [array enumerateObjectsWithOptions:usingBlock:] 如果你的代码受益于并行执行

这些方法可能不是最快的,但他们都是非常清晰易读的。所以请记住,有时是在不写干净的代码,和快速的代码之间做出选择,你会发现,你可以在两个世界得到最好的。

转:http://www.oschina.net/translate/high-performance-collection-looping-objective-c

Objective-C 高性能的循环相关推荐

  1. Objective-C 高性能的循环遍历 forin - NSEnumerator - 枚举 优化

    Cocoa编程的一个通常的任务是要去循环遍历一个对象的集合  (例如,一个 NSArray, NSSet 或者是 NSDictionary). 这个看似简单的问题有广泛数量的解决方案,它们中的许多不乏 ...

  2. 高性能计算机软件循环流水,软件流水中循环展开优化.pdf

    2004 年 11 月 北 京 航 空航 天 大 学 学 报 November 2004 第 卷 第 期 30 11 JournaI of Beijing University of Aeronaut ...

  3. wxWidgets:制作渲染循环

    wxWidgets:制作渲染循环 wxWidgets:制作渲染循环 什么不该做 使用循环 在循环中使用 wxYield() 如果您得到奇怪的结果,需要检查的其他事项 使用空闲事件 使用定时器 wxWi ...

  4. 湖北省2022年工业废水循环利用试点申报条件、程序

    介绍武汉市2022年国家智能制造试点示范申报条件时间,又有哪些奖励政策呢,有需要申报或者了解政策可以和我们的项目人员沟通,十年项目申报经验,科技企业一站式孵化平台,从公司注册.初创企业到融资上市全称陪 ...

  5. iOS -- 学习资料总结(转载)

    先著名转载地址:https://www.jianshu.com/p/c47c24ab1e76/ 关于iOS学习进阶的必读一些博客总结 经过一周的思考还是决定重组一下优秀的博客,首次整理这些博客比较乱, ...

  6. 关于iOS学习进阶的必读一些博客总结

    关于iOS学习进阶的必读一些博客总结 作者 Raybon_lee 2016.01.16 22:28* 字数 3110 经过一周的思考还是决定重组一下优秀的博客,首次整理这些博客比较乱,但是后期会慢慢增 ...

  7. Linux下常用的C/C++开源Socket库

    1.      Linux Socket Programming In C++ : http://tldp.org/LDP/LG/issue74/tougher.html 2.      ACE: h ...

  8. 几种经典的网络服务器架构模型的分析与比较

    前言 事件驱动为广大的程序员所熟悉,其最为人津津乐道的是在图形化界面编程中的应用:事实上,在网络编程中事件驱动也被广泛使用,并大规模部署在高连接数高吞吐量的服务器程序中,如 http 服务器程序.ft ...

  9. 开源免费的C/C++网络库(c/c++ sockets library)

    (1)ACE 庞大.复杂,适合大型项目.开源.免费,不依赖第三方库,支持跨平台. http://www.cs.wustl.edu/~schmidt/ACE.html (2)Asio Asio基于Boo ...

  10. libev源码分析---整体设计

    libev是Marc Lehmann用C写的高性能事件循环库.通过libev,可以灵活地把各种事件组织管理起来,如:时钟.io.信号等.libev在业界内也是广受好评,不少项目都采用它来做底层的事件循 ...

最新文章

  1. C++中的类对象的内存分布以及虚函数表内存分布
  2. Django(part37)--session
  3. java关于泛型的实验代码_[改善Java代码]强制声明泛型的实际类型
  4. 计算机模拟分子设计,计算机模拟分子材料.pdf
  5. 三级菜单 ajax 已经测试成功
  6. Android 系统(216)---Android坐标分析过程
  7. android unit(px,dp,dip,sp)
  8. 面试鹅厂,我被虐的体无完肤。。。
  9. spss可以关键词词频分析吗_做关键词分析,我有4款免费词云工具
  10. y = mapminmax(‘apply‘,x,ps)中各个参数是什么意思呢?
  11. 关于Kindle的生词提示功能不显示
  12. SQLSERVER、ORACLE中根据汉字获取拼音函数
  13. 构造图片木马,绕过文件内容检测上传木马
  14. 域名dns被劫持怎么办、dns被劫持怎么办、dns被劫持的解决方法
  15. n以内的最多因子数的数 n=1e18
  16. TPshop登录接口测试
  17. 伽利略定位系统的历史
  18. 值得收藏的抖音唯美冬季古诗词文案分享
  19. 封闭式基金最新折价率排行(20060922)[ZT]
  20. php获取网址根目录,php获取当前目录_php获得网站根目录的几个方法

热门文章

  1. Atitit.各种 数据类型 ( 树形结构,表形数据 ) 的结构与存储数据库 attilax 总结
  2. Atom: 被墙,插件安装慢、安装困难 解决方案
  3. Octavia 加速 OpenStack LBaaS 落地大规模应用场景
  4. python整数运算定律_PHP 操作redis 详细讲解
  5. 【运动学】基于matlab GUI模拟鱼类目标追击问题【含Matlab源码 166期】
  6. 毕设题目:Matlab优化电价
  7. 【优化预测】基于matlab萤火虫算法优化BP神经网络预测【含Matlab源码 1313期】
  8. 【TWVRP】基于matlab遗传算法求解送货且带时间窗的车辆路径规划问题【含Matlab源码 1074期】
  9. 【语音识别】基于matlab GUI语音识别信号灯图像模拟控制(带面板)【含Matlab源码 757期】
  10. 【语音编码】基于matlab PCM编解码【含Matlab源码 555期】