Swift的性能:map()和reduce()vs for循环
我在Swift中编写了一些性能关键的代码。 在实现了我所能想到的所有优化之后,并在Instruments中分析应用程序之后,我意识到绝大多数CPU周期都花在对Floats数组执行map()
和reduce()
操作上。 所以,为了看看会发生什么,我将所有的map
实例都replace成了旧的for
循环。 令我惊讶的是, for
循环要快得多!
有点困惑,我决定执行一些粗糙的基准。 在一个testing中,在执行一些简单的算术之后,我有map
返回一个Float数组:
// Populate array with 1,000,000,000 random numbers var array = [Float](count: 1_000_000_000, repeatedValue: 0) for i in 0..<array.count { array[i] = Float(random()) } let start = NSDate() // Construct a new array, with each element from the original multiplied by 5 let output = array.map({ (element) -> Float in return element * 5 }) // Log the elapsed time let elapsed = NSDate().timeIntervalSinceDate(start) print(elapsed)
和等价for
循环实现:
var output = [Float]() for element in array { output.append(element * 5) }
map
平均执行时间:20.1秒。 for
循环的平均执行时间:11.2秒。 使用整数而不是浮点的结果是相似的。
我创build了一个类似的基准来testingSwift的reduce
性能。 这次,求和一个大arrays的元素时, reduce
和for
循环达到了几乎相同的性能。 但是当我这样循环testing10万次时:
// Populate array with 1,000,000 random numbers var array = [Float](count: 1_000_000, repeatedValue: 0) for i in 0..<array.count { array[i] = Float(random()) } let start = NSDate() // Perform operation 100,000 times for _ in 0..<100_000 { let sum = array.reduce(0, combine: {$0 + $1}) } // Log the elapsed time let elapsed = NSDate().timeIntervalSinceDate(start) print(elapsed)
VS:
for _ in 0..<100_000 { var sum: Float = 0 for element in array { sum += element } }
reduce
方法需要29秒,而for
循环需要(显然)0.000003秒。
当然,我已经准备好不顾最后一次testing作为编译器优化的结果了,但是我认为它可以让我们对编译器如何针对循环和Swift的内置数组方法进行不同的优化有所了解。 请注意,所有testing都是在2.5 GHz i7 MacBook Pro上进行了-Os优化。 结果取决于数组大小和迭代次数,但循环总是胜过其他方法至less1.5倍,有时甚至高达10倍。
对于Swift在这里的performance我有点困惑。 内置的Array方法不应该比执行这种操作的简单方法更快吗? 也许比我有更多低级知识的人可以对情况有所了解。
内置的Array方法不应该比执行这种操作的简单方法更快吗? 也许比我有更多低级知识的人可以对情况有所了解。
我只想试图从概念层面(对Swift的优化器的本质理解不甚了解)和“不一定”这两个问题进行解决。 它来自于编译器devise和计算机体系结构的背景,而不是深入了解Swift优化器性质的知识。
调用开销
像map
这样的函数和reduce
接受函数作为input来reduce
,这使得优化器把它放在一个更大的压力。 在这种情况下,一些非常激进的优化的自然诱惑就是不断地在map
的实现和你提供的闭包之间来回切换,同样在这些不同的代码分支之间传输数据(通过寄存器和堆栈通常)。
对于优化器来说,这种分支/调用的开销是非常困难的,尤其是考虑到Swift的closures的灵活性(不是不可能的,但在概念上相当困难)。 C ++优化器可以内联函数对象调用,但是要有更多的限制和代码生成技术,在编译器实际上必须为每个types的函数对象生成一组全新的指令时,指示用于代码生成的function模板的程序员)。
因此,发现您的手轮循环可以执行得更快 – 这不会让您大吃一惊,因为它们使优化器的负担减轻了很多。 我曾经看到一些人认为,这些高阶函数应该能够更快地进行,因为供应商能够做并行化循环,但是为了有效地并行化循环,首先需要那种典型的信息允许优化器将嵌套函数调用内联到一个点,在这个点上,它们就像手卷循环一样便宜。 否则,你传入的函数/闭包实现将对map/reduce
等函数有效不透明:它们只能调用它并支付这样的开销,并且不能并行化,因为它们不能假设任何关于边效果和线程安全性。
当然,这完全是概念上的 – Swift可能能够在未来优化这些情况,或者现在可能已经能够做到这一点(参见-Ofast
作为一个被广泛引用的方式,使得Swift以一些代价安全)。 但是它确实给优化器带来了更大的压力,至less在手动循环中使用这些函数,并且在第一个基准testing中看到的时间差异似乎反映了可能的差异期望与此额外的通话开销。 最好的方法是查看程序集并尝试各种优化标志。
标准函数
这并不妨碍使用这些function。 他们做得更简洁明了,可以提高生产力。 依靠它们可以让你的代码在未来版本的Swift中变得更快,而不需要你的参与。 但是它们并不总是会变得更快 – 这是一个很好的通用规则,认为更直接地expression你想要做的事情的更高级的库函数将会更快,但总是有例外规则(但最好是事后才能发现的,因为在信任方面比在这里不信任要好得多)。
人工基准
至于你的第二个基准testing,几乎可以肯定是编译器优化掉了没有副作用影响用户输出的代码的结果。 人为的基准testing有一个出乎意料的误导,因为优化器会消除不相关的副作用(本质上不影响用户输出的副作用)。 因此,在构build基准testing时,您必须小心谨慎,因为时间似乎太好了,以致于不是优化器仅仅是跳过所有您想要进行基准testing的工作。 至less,你希望你的testing输出从计算中收集到的最终结果。
我不能多说你的第一个testing( map()
vs append()
在循环中),但我可以确认你的结果。 如果添加,则追加循环变得更快
output.reserveCapacity(array.count)
数组创build后。 看来苹果可以在这里改进一些东西,你可能会提交一个错误报告。
在
for _ in 0..<100_000 { var sum: Float = 0 for element in array { sum += element } }
编译器(可能)删除整个循环,因为计算结果根本不使用。 我只能推测为什么类似的优化不会发生
for _ in 0..<100_000 { let sum = array.reduce(0, combine: {$0 + $1}) }
但是更难确定是否用闭包调用reduce()
有没有副作用。
如果testing代码稍微改变以计算并打印总和
do { var total = Float(0.0) let start = NSDate() for _ in 0..<100_000 { total += array.reduce(0, combine: {$0 + $1}) } let elapsed = NSDate().timeIntervalSinceDate(start) print("sum with reduce:", elapsed) print(total) } do { var total = Float(0.0) let start = NSDate() for _ in 0..<100_000 { var sum = Float(0.0) for element in array { sum += element } total += sum } let elapsed = NSDate().timeIntervalSinceDate(start) print("sum with loop:", elapsed) print(total) }
那么这两个变种在我的testing中大概需要10秒。