在Swift中,哪个循环更快,`for`还是`for-in`?为什么?

4

当需要极度关注迭代大型数组所需时间时,应使用哪个循环?


4
你有尝试去进行测量吗? - Martin R
2
请查看此链接:http://stackoverflow.com/questions/1364603/performance-of-comparisons-in-c-foo-0-vs-foo-0/1364614#1364614 - Dániel Nagy
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Krishna
我真的被扣了-1分吗? - Krishna
3
我没有进行负面评价,但这实际上很简单。让你的循环在两种情况下分别运行一百万次。在这种情况下,您可以使用手表来测量时间差异。如果没有差异,那就怎么回事? - qwerty_so
4个回答

17

简短回答

不要进行微观优化 - 在循环内执行的操作速度可能完全超过了任何差异。如果你确实认为这个循环是性能瓶颈,也许你最好使用类似 加速框架 的东西 - 但只有当分析表明这样做真正值得时才这么做。

并且不要与语言对抗。使用for…in,除非你想要实现的目标不能用for…in表达。这种情况很少见。使用for…in的好处在于,很难做错。这比速度更重要。正确性是重要的。你甚至可以完全跳过for循环而使用mapreduce

更详细的回答

对于数组,如果你没有使用最快的编译器优化,它们的性能是相同的,因为它们本质上是一样的。

假设你的 for ;;循环看起来像这样:

var sum = 0
for var i = 0; i < a.count; ++i {
    sum += a[i]
}

而你的for…in循环应该像这样:

for x in a {
    sum += x
}

让我们重写 for…in,以展示其底层的真实运行方式:

var g = a.generate()
while let x = g.next() {
    sum += x
}

然后让我们重写一下 a.generate() 返回的内容,以及像 let 所做的事情:

 var g = IndexingGenerator<[Int]>(a)
 var wrapped_x = g.next()
 while wrapped_x != nil {
     let x = wrapped_x!
     sum += x
     wrapped_x = g.next()
 }

下面是 IndexingGenerator<[Int]> 的实现示例:

struct IndexingGeneratorArrayOfInt {
    private let _seq: [Int]
    var _idx: Int = 0

    init(_ seq: [Int]) {
        _seq = seq
    }

    mutating func generate() -> Int? {
        if _idx != _seq.endIndex {
            return _seq[_idx++]
        }
        else {
            return nil
        }
    }
}

哇,那是很多的代码,肯定比普通的 for ;; 循环执行得慢!

不是这样的。虽然逻辑上可能是那样,但编译器有很大的自主优化余地。例如,注意到 IndexingGeneratorArrayOfInt 是一个struct而不是class。这意味着它在声明这两个成员变量时没有开销。编译器也可能能够内联 generate 中的代码——这里没有间接引用,没有重载的方法和 vtable 或 objc_MsgSend。只有一些简单的指针算术和解除引用。如果你去掉所有结构体和方法调用的语法,你会发现 for…in 的代码最终几乎与 for ;; 循环做的事情完全一样。

for…in 有助于避免性能错误

另一方面,如果对于开始给出的代码,你将编译器优化切换到更快的设置,for…in 看起来要比 for ;; 快得多。在我运行使用 XCTestCase.measureBlock 测试的一些非科学测试中,求和一组随机数的大数组,它的速度快了一个数量级。

为什么?因为使用了 count

for var i = 0; i < a.count; ++i {
                // ^-- calling a.count every time...
    sum += a[i]
}

或许优化器可以替你修复这个问题,但在这种情况下它并没有。如果你把不变量提取出来,那么速度就会恢复与for...in相同的水平:

let count = a.count
for var i = 0; i < count; ++i {
    sum += a[i]
}

“哦,我肯定每次都会这样做,所以没关系。”我说,真的吗?你确定吗?打赌有时候你可能会忘记。

但更好的消息是什么?用 reduce 做相同的总和计算(在我的测试中,虽然不太科学)比使用for循环更快:

let sum = a.reduce(0,+)

但它也更加表达力强,易读(在我看来),并允许您使用let声明结果。既然这应该是您的主要目标,那么速度就是额外的奖励。但希望性能能给您一个动力,无论如何都要使用它。

这只是针对数组,那其他集合呢?当然这取决于实现,但有足够的理由相信它会比其他集合如字典、自定义用户定义的集合更快。

我的理由是,集合的作者可以实现优化版本的generate,因为他们确切地知道集合的使用方式。假设下标查找涉及某些计算(例如在数组的情况下进行指针算术-必须将索引乘以值大小,然后将其加到基址指针上)。在生成的情况下,您知道正在顺序遍历集合,因此您可以为此进行优化(例如,在数组的情况下,保存指向下一个元素的指针,每次调用next时将其递增)。同样适用于专门的成员版本reducemap

这甚至可能是为什么数组上执行reduce表现出色的原因-谁知道呢(如果您想尝试并找出,请在传递的函数上放置断点)。但这只是另一个使用应该使用的语言构造的理由。


5
著名的Donald Knuth曾经说过:“我们应该忘记小的效率问题,大约97%的时间:过早优化是万恶之源。” 看起来你不可能是那3%。
专注于手头更大的问题。在它正常运行后,如果需要提高性能,则可以考虑for循环。但我向你保证,在最终,更大的结构性低效或不良算法选择将成为性能问题,而不是for循环。
担心for循环已经过时了。

1

就我个人而言,一个基本的游乐场测试显示,map()比枚举大约快10倍:

class SomeClass1 {
  let value: UInt32 = arc4random_uniform(100)
}
class SomeClass2 {
  let value: UInt32
  init(value: UInt32) {
    self.value = value
  }
}
var someClass1s = [SomeClass1]()
for _ in 0..<1000 {
  someClass1s.append(SomeClass1())
}

var someClass2s = [SomeClass2]()
let startTimeInterval1 = CFAbsoluteTimeGetCurrent()
someClass1s.map { someClass2s.append(SomeClass2(value: $0.value)) }
println("Time1: \(CFAbsoluteTimeGetCurrent() - startTimeInterval1)") // "Time1: 0.489435970783234"
var someMoreClass2s = [SomeClass2]()
let startTimeInterval2 = CFAbsoluteTimeGetCurrent()
for item in someClass1s { someMoreClass2s.append(SomeClass2(value: item.value)) }
println("Time2: \(CFAbsoluteTimeGetCurrent() - startTimeInterval2)") // "Time2 : 4.81457495689392"

0

使用计数器的for循环只是在递增计数器。非常快速。for-in循环使用迭代器(调用对象以传递下一个元素)。这会慢得多。但最终你想要访问两种情况下的元素,这最终不会有任何区别。


“for-in 循环使用迭代器(调用对象以传递下一个元素)。这会导致速度较慢。”——实际情况很可能相反,请参见我的答案。 - Airspeed Velocity
我的总结是“最终没有区别”,这就是我想要表达的观点。基本上,我会同意@GoZoner的看法。出于这些原因,我在帖子中没有花费太多精力。我的意思是:循环不重要,它取决于循环体内的内容。 - qwerty_so

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接