我在使用Swift Beta实现一个算法时发现性能非常差。经过深入挖掘,我意识到其中一个瓶颈是像排序数组这样简单的事情。相关部分如下:
let n = 1000000
var x = [Int](repeating: 0, count: n)
for i in 0..<n {
x[i] = random()
}
// start clock here
let y = sort(x)
// stop clock here
在C++中,一个类似的操作在我的电脑上只需要0.06秒。
在Python中,对于一个整数列表,执行y = sorted(x)这个操作需要0.6秒(没有任何技巧)。
在Swift中,如果我使用以下命令编译,则需要6秒:
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
如果我使用以下命令编译它,需要长达88秒的时间:
xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`
"发布版"和"调试版"在Xcode中的时间差别相似。
这里有什么问题吗?我可以理解与C++相比会有一些性能损失,但是与纯Python相比出现10倍的减速。
编辑:weather注意到将-O3
更改为-Ofast
使得该代码运行速度几乎与C ++版本相同!但是,-Ofast
会大量改变语言的语义 - 在我的测试中,它禁用了整数溢出和数组索引溢出的检查。例如,使用-Ofast
,以下Swift代码将静默运行而不崩溃(并打印出一些垃圾):
let n = 10000000
print(n*n*n*n*n)
let x = [Int](repeating: 10, count: n)
print(x[n])
所以,-Ofast
不是我们想要的;Swift 的整个重点就是我们已经有了安全网。当然,这些安全网会对性能产生一定影响,但它们不应该让程序变慢 100 倍。请记住,Java 已经检查了数组边界,在典型情况下,减速因素远小于 2。而在 Clang 和 GCC 中,我们有 -ftrapv
来检查(带符号的)整数溢出,它也不是那么慢。因此,问题来了:我们如何在不失去安全保障的前提下获得合理的 Swift 性能?
编辑 2:我做了更多的基准测试,使用非常简单的循环,例如
for i in 0..<n {
x[i] = x[i] ^ 12345678
}
(这里使用异或操作只是为了更容易地在汇编代码中找到相关的循环。我尝试选择一种易于识别但也“无害”的操作,这意味着它不应需要与整数溢出相关的任何检查。)
同样,-O3
和-Ofast
之间的性能差异非常大。因此,我查看了汇编代码:
使用
-Ofast
得到的基本符合我的预期。相关部分是一个包含5个机器语言指令的循环。而使用
-O3
则超乎我的想象。内部循环跨越了88行汇编代码。我没有尝试理解所有内容,但最可疑的部分是13次调用“callq _swift_retain”和另外13次调用“callq _swift_release”。也就是说,在内部循环中有26次子程序调用!
编辑3:在评论中,Ferruccio要求公平的基准测试,这些测试不依赖于内置函数(例如排序)。我认为以下程序是一个相当好的例子:
let n = 10000
var x = [Int](repeating: 1, count: n)
for i in 0..<n {
for j in 0..<n {
x[i] = x[j]
}
}
没有算术,因此我们不需要担心整数溢出。我们所做的唯一事情就是大量的数组引用。而结果在这里——与 -Ofast
相比,Swift -O3 几乎慢了约 500 倍:
- C++ -O3: 0.05秒
- C++ -O0: 0.4秒
- Java: 0.2秒
- Python with PyPy: 0.5秒
- Python: 12秒
- Swift -Ofast: 0.05秒
- Swift -O3: 23秒
- Swift -O0: 443秒
(如果您担心编译器可能会完全优化掉无意义的循环,您可以将其更改为例如 x[i] ^= x[j]
并添加一个打印输出x[0]
的语句。这不会改变任何内容;计时将非常相似。)
是的,在这里,Python 实现是一个愚蠢的纯 Python 实现,使用 int 的列表和嵌套的 for 循环。它应该比未经优化的 Swift 慢得多。Swift 和数组索引似乎存在严重问题。
Edit 4: 这些问题(以及其他一些性能问题)似乎已在 Xcode 6 beta 5 中得到解决。
对于排序,我现在有以下计时:
- clang++ -O3: 0.06秒
- swiftc -Ofast: 0.1秒
- swiftc -O: 0.1秒
- swiftc: 4秒
对于嵌套循环:
- clang++ -O3: 0.06秒
- swiftc -Ofast: 0.3秒
- swiftc -O: 0.4秒
- swiftc: 540秒
看来现在没有理由再使用不安全的 -Ofast
(也称为-Ounchecked
)了;普通的-O
产生的代码同样出色。