我在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.06s。
在Python中,它花费0.6秒(绝招,仅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 = 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
}
(这里有xor操作,以便我可以更容易地在汇编代码中找到相关的循环。我试图选择一个易于发现但又“无害”的操作,因为它不需要任何相关的检查。到整数溢出。)
同样,-O3和之间的性能存在巨大差异-Ofast。所以我看了一下汇编代码:
有了-Ofast我,我得到了我所期望的。相关部分是一个包含5条机器语言指令的循环。
随着-O3我得到的东西,出乎我的想象。内部循环跨越88行汇编代码。我没有尝试理解所有内容,但是最可疑的部分是“ callq _swift_retain”的13个调用和“ callq _swift_release”的另外13个调用。也就是说,内部循环中有26个子例程调用!
编辑3:在评论中,费鲁奇奥要求提供不违反内置函数(例如排序)的公平基准。我认为以下程序是一个很好的例子:
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 s
C ++ -O0:0.4秒
Java:0.2秒
带有PyPy的Python:0.5秒
Python:12秒
迅捷-Ofast:0.05 s
Swift -O3:23秒
迅捷-O0:443秒
(如果担心编译器可能会完全优化无意义的循环,则可以将其更改为eg x[i] ^= x[j],并添加一个输出输出的print语句x[0]。这不会改变任何内容;时序将非常相似。)
是的,这里的Python实现是一个愚蠢的纯Python实现,带有一个整数列表和嵌套的for循环。这应该是很多比未优化雨燕慢。Swift和数组索引似乎严重破坏了某些东西。
江户川乱折腾
慕标5832272