我正在用Swift编写一些性能关键代码.在实现了我能想到的所有优化,并在仪器中分析了应用程序之后,我开始意识到,绝大多数CPU周期都花在对浮点数组执行map()reduce()操作上.所以,为了看看会发生什么,我用老式的for循环替换了mapreduce的所有实例.令我惊讶的是...for圈的速度要快得多!

对此有点困惑,我决定执行一些粗略的基准测试.在一次测试中,我让map人在执行以下简单算法后返回一个浮点数组:

// 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秒.用整数代替浮点数的结果是相似的.

我创建了一个类似的基准来测试Swift's reduce的性能.这一次,当对一个大型数组的元素求和时,reducefor个循环获得了几乎相同的性能.但当我像这样循环测试reduce000次时:

// 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秒.

当然,我准备忽略编译器优化后的最后一个测试,但我认为这可能会让我们更深入地了解编译器如何针对循环和Swift的内置数组方法进行不同的优化.请注意,所有测试都是在2.5 GHz i7 MacBook Pro上使用-Os优化进行的.结果因数组大小和迭代次数而异,但for个循环的性能总是比其他方法至少高出1.5倍,有时甚至高达10倍.

我对Swift 在这里的表现有点困惑.在执行此类操作时,内置数组方法不应该比天真的方法更快吗?也许有人比我更了解情况.

推荐答案

内置数组方法不应该比天真的方法更快吗

我只想try 从概念层面(我对Swift 优化器的本质知之甚少)用"不一定"来解决这部分问题.它更多地来自于编译器设计和计算机体系 struct 的背景,而不是关于Swift优化器本质的根深蒂固的知识.

Calling Overhead

由于像mapreduce这样的函数接受函数作为输入,这会给优化器带来更大的压力.在这种情况下,如果没有一些非常积极的优化,自然的诱惑是在(比如)map的实现和您提供的闭包之间不断地来回分支,并且同样地跨这些不同的代码分支(通常通过寄存器和堆栈)传输数据.

这种分支/调用开销对于优化器来说非常难以消除,尤其是考虑到Swift闭包的灵活性(并非不可能,但在概念上相当困难).C++优化器可以内嵌函数对象调用,但要做更多的限制和代码生成技术,编译器必须有效地为您传递的每种类型的函数对象生成一组新的map指令集.(在程序员的明确帮助下,指明用于代码生成的函数模板).

因此,如果发现手动循环可以执行得更快,也就不足为奇了——它们给优化器带来的压力大大降低了.我看到一些人提到,由于供应商能够并行化循环,但是,要有效地并行化循环,首先需要的信息通常会允许优化器将嵌套函数调用内联到一个与手动循环一样便宜的位置.否则,您传递的函数/闭包实现将对map/reduce这样的函数实际上是不透明的:它们只能调用它并支付这样做的开销,并且不能并行化它,因为它们不能假设这样做的副作用和线程安全性的性质.

当然,这都是概念性的——Swift可能在future 能够优化这些情况,或者现在已经能够优化这些情况(参见-Ofast,这是一种常用的方法,以牺牲一些安全性为代价,让Swift走得更快).但它确实给优化器带来了更大的压力,至少,在手动循环中使用这些函数是非常困难的,而且您在第一个基准测试中看到的时间差异似乎反映了在这种额外的调用开销下可能会出现的差异.最好的方法是查看程序集并try 各种优化标志.

Standard Functions

这并不是为了阻止使用这些函数.它们更简洁地表达意图,可以提高生产力.依靠它们可以让你的代码库在Swift的future 版本中变得更快,而不需要你的任何参与.但它们不一定总是更快——一般来说,一个更直接地表达你想要做什么的高级库函数会更快,但这条规则总是有例外(但最好是事后诸葛亮,因为在这里,在信任方面犯错比不信任要好得多).

Artificial Benchmarks

至于您的第二个基准测试,几乎可以肯定这是编译器优化代码的结果,它没有影响用户输出的副作用.由于优化器消除了不相关的副作用(本质上不影响用户输出的副作用),人工基准有一种误导的倾向.因此,在构建基准测试时,你必须小心,因为它们不是优化器跳过所有你真正想要基准测试的工作的结果.至少,您希望测试输出从计算中收集的一些最终结果.

Swift相关问答推荐

Swift中的www.example.com(qos:.background). c {}和Task(优先级:.background){}有什么DispatchQueue.global

Swift中可以取消的延迟任务

如何使用Swift宏向 struct 体及其init函数添加新成员?

如何在visionOS中进行购买?&# 39;购买(选项:)在visionOS中不可用

NavigationLink和ObservableObject的动画片段

使用 @resultBuilder 的通用 buildList 函数

Swift ui 转换无法按预期工作

如何在 switch case 模式语句中使用 Swift 文字正则表达式?

如果 Swift 无法分配内存会怎样?

如何使用带有 span 样式标签的 Swift XMLParser

闭包 - deinit self 对象的时间

SwiftUI Grid 中的数组旋转

Apple 的自然语言 API 返回意外结果

组合 flatMap 不会返回预期的上下文结果类型

仅在 Swift 中创建 Setter

为什么 swift 中的类没有存储类型属性?

Swift 3.0 的 stringByReplacingOccurencesOfString()

Swift 3:小数到 Int

用 UIBezierPath 画一条线

UISearchbar TextField 的高度可以修改吗?