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方法获取到所有键的一个副本. 这都将能很好的运作起来:

  // NSEnumerator
  id 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 enumeration
  for (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:] 如果你的代码受益于并行执行

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

 
 

Objective-C 高性能的循环的更多相关文章

  1. 高性能JavaScript 循环语句和流程控制

    前言 上一篇探讨了达夫设备对于代码性能的影响,本文主要探讨并且测试各种常见的循环语句的性能以及流程控制中常见的优化. 循环语句 众所周知,常用的循环语句有for.while.do-while以及for ...

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

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

  3. 1-3 - C#语言习惯 - 推荐使用查询语法而不是循环

    C#语言中并不缺少控制程序流程的结构,for.while.do-while和foreach等都可以做到这点. 历史上所有计算机语言设计者都不曾遗漏这些重要的循环控制结构. 不过我们还有一个更好的方式: ...

  4. 几种服务器端IO模型的简单介绍及实现

    一些概念: 同步和异步 同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发I/O操作并等待或者轮询的去查看I/O操作是否就绪,而异步是指用户进程触发I/O操作以后便开始做自己的事情,而 ...

  5. Coding源码学习第三部分(EaseStartView.m)

    首先接上篇的要做一个NSEnumerator 类的延展阅读. 枚举(NSEnumerator) (1)依附于集合类(NSArray,NSSet,NSDictionary),没有用来创建实例的接口. ( ...

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

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

  7. Swift - 多线程实现方式(3) - Grand Central Dispatch(GCD)

    1,Swift继续使用Object-C原有的一套线程,包括三种多线程编程技术: (1)NSThread (2)Cocoa NSOperation(NSOperation和NSOperationQueu ...

  8. Linux下套接字具体解释(三)----几种套接字I/O模型

    參考: 网络编程–IO模型演示样例 几种server端IO模型的简介及实现 背景知识 堵塞和非堵塞 对于一个套接字的 I/O通信,它会涉及到两个系统对象.一个是调用这个IO的进程或者线程,还有一个就是 ...

  9. [转]Libev源码分析 -- 整体设计

    Libev源码分析 -- 整体设计 libev是Marc Lehmann用C写的高性能事件循环库.通过libev,可以灵活地把各种事件组织管理起来,如:时钟.io.信号等.libev在业界内也是广受好 ...

随机推荐

  1. poj 3841 Double Queue (AVL树入门)

    /****************************************************************** 题目: Double Queue(poj 3481) 链接: h ...

  2. html file控件选择文件后立即预览 js实现

    //上传图片后立即预览 file对象,图片容器id function showImg(fileObj,imgId) { var file=fileObj.files[0]; var r = new F ...

  3. Eclipse 反编译插件安装jad

    Eclipse的反编译插件一直在用jad,感觉很不错. 刚下了个新版的eclipse,配置jad的时候发现要多设置个东西. 从头开始 环境介绍: eclipse version:Kepler Rele ...

  4. Til the Cows Come Home

    Description Bessie is out in the field and wants to get back to the barn to get as much sleep as pos ...

  5. [Chapter 3 Process]Practice 3.3 Discuss three major complications that concurrent processing adds to an operating system.

    3.3  Original version of Apple's mobile iOS operating system provied no means of concurrent processi ...

  6. 斯坦福第十六课:推荐系统(Recommender Systems)

    16.1  问题形式化 16.2  基于内容的推荐系统 16.3  协同过滤 16.4  协同过滤算法 16.5  矢量化:低秩矩阵分解 16.6  推行工作上的细节:均值归一化 16.1  问题形式 ...

  7. 斯坦福第十三课:聚类(Clustering)

    13.1  无监督学习:简介 13.2 K-均值算法 13.3  优化目标 13.4  随机初始化 13.5  选择聚类数 13.1  无监督学习:简介 在这个视频中,我将开始介绍聚类算法.这将是一个 ...

  8. Python time datetime常用时间处理方法

    常用时间转换及处理函数: import datetime # 获取当前时间 d1 = datetime.datetime.now() print d1 # 当前时间加上半小时 d2 = d1 + da ...

  9. Dynamic CRM 2013学习笔记(十八)根据主表状态用JS控制子表自定义按钮

    有时要根据主表的审批状态来控制子表上的按钮要不要显示,比如我们有一个需求审批通过后就不能再上传文件了. 首先打开Visual Ribbon Editor, 如下图,我们可以利用Enable Rules ...

  10. 团队项目 SRS文档

    一:实验内容:用例模型的建立 简介: 图书管理系统是使用计算机实现图书大量信息处理的电子档案管理系统,在本系统中主要满足借书者.图书管理员和系统管理员3方面的需求.对借书者来说主要是查询个人信息.查询 ...