Objective-C 高性能的循环

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

对于速度的追求

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

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

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


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

 

 

经典的循环方式

?
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的更便利(而且明显更快速) 的替代方法而引入的. 它并无使得枚举类过期由于其仍然被应用于注入反向枚举, 或者是当你须要对集合进行变动操做 (以后会更多地提到) 这些场景中.编码

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

?
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
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:] 若是你的代码受益于并行执行

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

        
 
相关文章
相关标签/搜索