我在Swift Beta中实现了一个算法,注意到性能非常差。深入挖掘后,我意识到其中一个瓶颈是排序数组这样简单的东西。相关部分在这里:
let n = 1000000var x = [Int](repeating: 0, count: n)for i in 0..<n {x[i] = random()}// start clock herelet y = sort(x)// stop clock here
C++,类似的操作在我的计算机上执行0.06s。
在Python中,它需要0.6s(没有技巧,只是y=sorted(x)作为整数列表)。
在Swift中,如果我使用以下命令编译它,则需要6s:
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
如果我使用以下命令编译它,它需要尽可能多的88s:
xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`
Xcode中带有“发布”和“调试”版本的时间是相似的。
这里有什么问题?与C++相比,我可以理解一些性能损失,但与纯Python相比,速度下降了10倍。
编辑:天气注意到,将-O3
更改为-Ofast
使此代码的运行速度几乎与C++版本一样快!然而,-Ofast
改变了该语言的语义学很多-在我的测试中,它禁用整数溢出和数组索引溢出检查。例如,使用-Ofast
,以下Swift代码静默运行而不崩溃(并打印出一些垃圾):
let n = 10000000print(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}
(这里的xor操作只是为了更容易地在汇编代码中找到相关的循环。我试图选择一个容易发现但也“无害”的操作,因为它不需要任何与整数溢出相关的检查。)
同样,-O3
和-Ofast
之间的性能存在巨大差异。所以我看了一下汇编代码:
使用-Ofast
,我几乎得到了我所期望的。相关部分是包含5条机器语言指令的循环。
使用-O3
,我得到了一些超出我最疯狂想象的东西。内部循环跨越88行汇编代码。我并没有试图理解所有的代码,但最可疑的部分是13次调用“call q_swift_retain”和另外13次调用“call q_swift_release”。也就是说,内部循环中有26个子例程调用!
编辑3:在评论中,Ferruccio要求基准测试是公平的,因为它们不依赖于内置函数(例如排序)。我认为以下程序是一个相当好的例子:
let n = 10000var x = [Int](repeating: 1, count: n)for i in 0..<n {for j in 0..<n {x[i] = x[j]}}
没有算术,所以我们不需要担心整数溢出。我们唯一要做的就是大量的数组引用。结果在这里-与-OFast相比,Swift-O3损失了近500倍:
(如果您担心编译器可能会完全优化无意义的循环,您可以将其更改为例如x[i] ^= x[j]
,并添加输出x[0]
的print语句。这不会改变任何事情;时间将非常相似。)
是的,这里的Python实现是一个愚蠢的纯Python实现,带有整数列表和嵌套循环。它应该比未优化的Swift慢多。Swift和数组索引似乎严重破坏了一些东西。
编辑4:这些问题(以及其他一些性能问题)似乎已在Xcode 6 beta 5中得到修复。
对于排序,我现在有以下时间:
对于嵌套循环:
似乎没有理由再使用不安全的-Ofast
(又名-Ounchecked
);普通的-O
产生同样好的代码。