Swift测试版性能:排序数组

我在Swift Beta版中实现了一个算法,发现性能非常差。在深入挖掘之后,我意识到瓶颈之一就是排序数组这样简单的事情。有关部分如下:

设n=1000000
变量x=[Int](重复:0,计数:n)
对于0中的i..<n{
x[i]=random()
}
//从这里开始计时
设y=sort(x)
//在这里停车

在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中“Release”和“Debug”版本的计时是相似的

这里怎么了?与C++相比,我可以理解一些性能损失,但与纯Python相比,它并不是10倍的减速。


> >编辑:天气注意到,将 -O3 > -OFAST使此代码运行速度几乎与C++版本一样快!然而,-Ofast极大地改变了语言的语义-在我的测试中,它禁用了整数溢出和数组索引溢出的检查。例如,使用-Ofast时,以下Swift代码以静默方式运行,不会崩溃(并打印出一些垃圾):

让n=10000000
打印(n*n*n*n*n)
设x=[Int](重复:10,计数:n)
打印(x[n])

所以,我们不想要的不是ast的;Swift的全部意义在于我们已经建立了安全网。当然,安全网对性能有一定影响,但它们不应使程序的速度降低100倍。请记住,Java已经检查了数组边界,在典型情况下,速度降低了一倍,远远小于2。在Clang和GCC中,我们得到了用于检查(有符号)整数溢出的-ftrapv,而且速度也不慢

因此,问题是:我们如何在不失去安全网的情况下,在Swift中获得合理的性能


编辑2:我做了更多的基准测试,沿着

0中的i的

。<n{
x[i]=x[i]^12345678
}

(这里的xor操作只是为了更容易在汇编代码中找到相关的循环。我尝试选择一个容易发现但“无害”的操作,因为它不需要任何与整数溢出相关的检查。)

同样,在-O3-Ofast之间的性能存在巨大差异。所以我看了一下汇编代码:

  • 有了Ofast的-of,我得到了我所期望的东西。相关部分是一个包含5条机器语言指令的循环

  • 通过-O3我得到了超出我想象的东西。内部循环跨越88行汇编代码。我并没有试图理解所有的内容,但最可疑的部分是13次调用“callq\u swift\u retain”和另外13次调用“callq\u swift\u release”。也就是说,26个子例程在内部循环中调用


编辑3:在评论中,Ferruccio要求基准测试是公平的,因为它们不依赖内置功能(例如排序)。我认为以下程序是一个相当好的例子:

设n=10000
变量x=[Int](重复:1,计数:n)
对于0中的i..<n{
对于0中的j..<n{
x[i]=x[j]
}
}

没有算术运算,所以我们不需要担心整数溢出。我们唯一要做的就是大量的数组引用。结果显示,与-Ofast相比,Swift-O3损失了近500倍:

  • C++-O3:0.05秒
  • C++-O0:0.4秒
  • Java:0.2秒
  • 带PyPy的Python:0.5s
  • Python:12s
  • Swift-Ofast:0.05秒
  • Swift-O3:23秒
  • Swift-O0:443秒

(如果您担心编译器可能会完全优化无意义的循环,您可以将其更改为例如x[i]^=x[j],并添加一个输出x[0]的打印语句。这不会改变任何事情;计时将非常相似。)

是的,这里的Python实现是一个愚蠢的纯Python实现,它有一个int列表和嵌套for循环。它应该比未优化的Swift慢很多。Swift和数组索引似乎严重破坏了某些功能


编辑4:这些问题(以及其他一些性能问题)似乎已在Xcode 6 beta 5中修复

对于排序,我现在有以下时间安排:

  • 叮当声++-O3:0.06秒
  • swiftc-Ofast:0.1秒
  • swiftc-O:0.1秒
  • swiftc:4秒

对于嵌套循环:

  • 叮当声++-O3:0.06秒
  • swiftc-Ofast:0.3秒
  • swiftc-O:0.4秒
  • swiftc:540秒

似乎再也没有理由使用不安全的Ofast-Ofast(又称“Ounchecked”);普通的-O产生同样好的代码

tl;dr Swift 1.0现在通过使用默认版本优化级别[-O],与C一样快


以下是Swift测试版中的就地快速排序:

func快速排序\u swift(inout a:CInt[],开始:Int,结束:Int){
如果(结束-开始<2){
回来
}
var p=a[start+(end-start)/2]
var l=开始
var r=end-1
而(l<=r){
如果(a[l]<p){
l+=1
持续
}
if(a[r]>p){
r-=1
持续
}
var t=a[l]
a[l]=a[r]
a[r]=t
l+=1
r-=1
}
快速排序(a、开始、r+1)
快速排序(a、r+1、end)
}

在C中也是一样的:

无效快速排序(int*a,int-n){
如果(n<2)
回来
int p=a[n/2];
int*l=a;
int*r=a+n-1;
而(l<=r){
如果(*l<p){
l++;
持续
}
如果(*r>p){
r--;
持续
}
int t=*l;
*l++=*r;
*r--=t;
}
快速排序c(a,r-a+1);
快速排序c(l,a+n-l);
}

这两项工作:

var a_swift:CInt[]=[0,5,2,81234,-1,2]
变量a_c:CInt[]=[0,5,2,81234,-1,2]
快速排序(a_-swift,0,a_-swift.count)
快速排序(a&a_c,CInt(a_c.计数))
// [-1, 0, 2, 2, 5, 8, 1234]
// [-1, 0, 2, 2, 5, 8, 1234]

两者都在编写的同一程序中调用

var x_swift=CInt[](计数:n,重复值:0)
变量x_c=CInt[](计数:n,重复值:0)
对于var i=0;我<n++我{
x_swift[i]=CInt(random())
x_c[i]=CInt(random())
}
让快速启动:UInt64=马赫绝对时间();
快速排序(和x\u swift,0,x\u swift.count)
让swift_停止:UInt64=马赫绝对时间();
让c_开始:UInt64=马赫绝对时间();
快速排序(和x_c,CInt(x_c.计数))
让c_停止:UInt64=马赫绝对时间();

这会将绝对时间转换为秒:

静态常数64纳米/秒c=1000ULL;
静态常数64毫微秒=1000ULL*毫微秒/秒;
静态常数64纳米/秒=1000ULL*纳米/毫秒;
马赫时基信息数据时基信息;
uint64\u t abs\u to\u nanos(uint64\u t abs){
如果(时基信息=0){
(无效)马赫时基信息(和时基信息);
}
返回abs*timebase\u info.numer/timebase\u info.denom;
}
双abs至双abs秒(uint64-t abs){
每秒将abs_返回到_nanos(abs)/(双)nanos_;
}

以下是编译器优化级别的摘要:

[-Onone]无优化,默认为调试。
[-O]执行优化,这是发布的默认设置。
[-Ofast]执行优化并禁用运行时溢出检查和运行时类型检查。

使用[-Onone]表示n=10_000的时间(秒):

Swift:0.895296452
C:0.001223848

这是斯威夫特的内置排序()用于n=10000

Swift\u内置:0.77865783

这里是[-O]n=10000

Swift:0.045478346
C:0.000784666
Swift_内置:0.032513488

如您所见,Swift的性能提高了20倍

根据Mweather的回答,设置[-Ofast]会产生真正的差异,从而导致n=10_000的这些时间:

Swift:0.000706745
C:0.000742374
Swift_内置:0.000603576

对于n=1_000_000

Swift:0.107111846
C:0.114957179
Swift_排序:0.092688548

为便于比较,这与[-Onone]中的n=1\u 000

Swift:142.659763258
C:0.162065333
Swift_排序:114.095478272

在这个基准测试中,在开发的这个阶段,没有优化的Swift几乎比C慢1000倍。另一方面,当两个编译器都设置为[-Ofast]时,Swift的实际性能至少与C一样好

有人指出,[-Ofast]改变了语言的语义,使其具有潜在的不安全性。这是苹果在Xcode 5.0发行说明中所说的:

LLVM中提供了一个新的优化级别—Ofast,支持积极的操作

发表评论