非常详细,原文点这里:
Cocoa编程的一个通常的任务是要去循环遍历一个对象的集合 (例如,一个 NSArray, NSSet 或者是 NSDictionary). 这个看似简单的问题有广泛数量的解决方案,它们中的许多不乏有对性能方面问题的细微考虑. 对于速度的追求首先,是一个免责声明: 相比其它问题而言,一个 Objective-C 方法原始的速度是你在编程时最后才需要考虑的问题之一 – 区别就在于这个问题够不上去同其它更加需要重点考虑的问题进行比较,比如说代码的清晰度和可读性. 但速度的次要性并不妨碍我们去理解它. 你应该经常去了解一下性能方面的考虑将如何对你正在编写的代码产生影响,一边在极少数发生问题的情况下,你会知道如何下手. 还有,在循环的场景中,大多数时候不管是从可读性或者是清晰度考虑,你选择哪种技术都没什么关系的, 所以你还不如选择速度最快的那一种. 没有必要选择编码速度比要求更慢的。 |
leoxu
|
考虑到这一点,就有了如下的选择:
经典的循环方式
这是循环遍历一个数组的一个简单熟悉的方式; 从性能方面考虑它也相当的差劲. 这段代码最大的问题就是循环每进行一次我们都会调用数组的计数方法. 数组的总数是不会改变的,因此每次都去调用一下这种做法是多余的. 像这种代码一般C编译器一般都会优化掉, 但是 Objective-C 的动态语言特性意味着对这个方法的调用不会被自动优化掉. 因此,为了提升性能,值得我们在循环开始之前,将这个总数存到一个变量中,像这样:
|
leoxu
|
NSEnumerator
|
|
NSEnumerator 的性能可以媲美原生的for循环, 但它更加实用,因为它对索引的概念进行了抽象,这意味着它应用在结构化数据上,比如链表,或者甚至是无穷序列和数据流,这些结构中的数据条数未知或者并没有被定义.
leoxu
翻译于 2年前
0人顶
顶 翻译的不错哦!
快速枚举快速枚举是在 Objective-C 2.0 中作为传统的NSEnumerator的更便利(并且明显更快速) 的替代方法而引入的. 它并没有使得枚举类过时因为其仍然被应用于注入反向枚举, 或者是当你需要对集合进行变更操作 (之后会更多地提到) 这些场景中. 快速枚举添加了一个看起来像下面这样子的新的枚举方法:
如果你正在想着“那看起来并不怎么舒服啊!”, 我不会怪你的. 但是新的方法顺便带来了一种新的循环语法, for…in 循环. 这是在幕后使用了新的枚举方法, 并且重要的是在语法和性能上都比使用传统的for循环或者 NSEnumerator 方法都更省心了:
|
leoxu
| ||||||
其它翻译版本(1) |
枚举块
|
leoxu
|
基准测试那么这些方法叠加起来会如何呢, 性能会更加的好么? 这里有一个简单的基准测试命令行应用,比较了使用多种不同方法枚举一个数据的性能. 我们已经在 ARC 关闭的情况下运行了它,以排除任何干扰最终结果的隐藏在幕后的保留或者排除处理. 由于是运行在一个很快的 Mac 机上面, 所有这些方法运行极快以至于我们实际上不得不使用一个存有10,000,000 (一千万) 对象的数组来测量结果. 如果你决定在一个 iPhone 进行测试, 最明智的做法是使用一个小得多的数量! 为了编译这段代码:
下面展示出了结果:
|
leoxu
|
忽略掉时间的具体长短. 我们感兴趣的是它们同其它方法比较的相对大小. 如果我们按顺序排列它们,快的放前面,我会得到了下面的结果:
For…in 是胜出者. 显然他们将其称为快速枚举是有原因的! 并发枚举看起来是比单线程的快一点点, 但是你没必要对其做更多的解读: 我们这里是在枚举一个非常非常大型的对象数组,而对于小一些的数据并发执行的开销远多于其带来的好处. 并发执行的主要是在当你的循环需要大量的执行时间时有优势. 如果你在自己的循环中有许多东西要运行,那就考虑试下并行枚举,在你不关心枚举顺序的前提下 (但是请用行动的去权衡一下它是否变得更快乐,不要空手去揣度). |
leoxu
|
其它集合类型Other Collection Types那么其它的结合类型怎么样呢, 比如 NSSet 和 NSDictionary? NSSet 是无序的, 因此没有按索引去取对象的概念.我们也可以进行一下基准测试:
结果同 NSArray 一致; for…in 再一次胜出了. NSDictionary怎么样了? NSDictionary 有一点不同因为我们同时又一个键和值对象需要迭代. 在一个字典中单独迭代键或者值是可以的, 但典型的情况下我们两者都需要. 这里我们有一段适配于操作NSDictionary的基准测试代码:
|
leoxu
|
NSDictionary 填充起来比 NSArray 或者 NSSet 慢得多, 因此我们把数据条数减少到了10,000 (一万) 以避免机器锁住. 因而你应该忽略结果怎么会比那些 NSArray 低那么多,因为我们使用的是更少对象的 1000 次循环:
没有优化过的循环再这里慢得很壮观,因为每一次我们都复制了键数组. 通过把键数组和总数存到变量中,我们获得了更快的速度. 查找对象的消耗现在主宰了其它的因素,因此使用一个for循环, NSEnumerator 或者for…in 差别很小. 但是对于枚举块方法而言,它在一个方法中把键和值都返回了,所以现在变成了最快的选择。 |
leoxu
|
反转齿轮基于我们所见,如果所有其它的因素都一样的话,在循环遍历数组时你应该尝试去使用for...in循环, 而遍历字典时,则应该选择枚举块. 也有一些场景下这样的做法并不可能行得通,比如我们需要回头来进行枚举,或者当我们在遍历时想要变更集合的情况. 为了回过头来枚举一个数据,我们可以调用reverseObjectEnumerator方法来获得一个NSEnumerator 以从尾至头遍历数组. NSEnumerator, 就像是 NSArray 它自己, 支持快速的枚举协议. 那就意味着我们仍然可以在这种方式下使用 for…in, 而无速度和简洁方面的损失:
(除非你异想天开, NSSet 或者 NSDictionary 是没有等效的方法的, 而反向枚举一个 NSSet 或者NSDictionary无论如何都没啥意义, 因为键是无序的.) 如果你想使用枚举块的话, NSEnumerationReverse你可以试试, 像这样:
|
leoxu
|
变更Mutation
|
leoxu
|
我们仍然可以使用for…in, 但前提是我们首先创建了一个数组的拷贝. 这会起作用的,例如:
如果我们对不同的技术进行基准测试(必要时把复制数组的开销算在内,以便我们可以对原来数组内的数据进行变更), 我们发现复制抵消了 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方法获取到所有键的一个副本. 这都将能很好的运作起来:
|
leoxu
|
然而同样的技术在使用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循环中根据键取对象的开销现在已经被在调用枚举块方法之前复制字典的开销盖过去了. |
leoxu
|
当枚举一个NSArray的时候:
当枚举一个NSSet的时候:
当枚举一个NSDictionary的时候:
这些方法可能不是最快的,但他们都是非常清晰易读的。所以请记住,有时是在不写干净的代码,和快速的代码之间做出选择,你会发现,你可以在两个世界得到最好的。 |