问题描述
我在 Swift Beta 中实现了一个算法,发现性能很差.在深入挖掘之后,我意识到瓶颈之一就是排序数组一样简单.相关部分在这里:
I was implementing an algorithm in Swift Beta and noticed that the performance was very poor. After digging deeper I realized that one of the bottlenecks was something as simple as sorting arrays. The relevant part is here:
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 秒.
In C++, a similar operation takes 0.06s on my computer.
在 Python 中,它需要 0.6 秒(没有技巧,对于整数列表只需 y = sorted(x)).
In Python, it takes 0.6s (no tricks, just y = sorted(x) for a list of integers).
在 Swift 中,如果我使用以下命令编译它需要 6s:
In Swift it takes 6s if I compile it with the following command:
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
如果我使用以下命令编译它需要 88 秒:
And it takes as much as 88s if I compile it with the following command:
xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`
Xcode 中发布"与调试"构建的时间相似.
Timings in Xcode with "Release" vs. "Debug" builds are similar.
这里有什么问题?与 C++ 相比,我可以理解一些性能损失,但与纯 Python 相比,速度并不能降低 10 倍.
What is wrong here? I could understand some performance loss in comparison with C++, but not a 10-fold slowdown in comparison with pure Python.
weather 注意到将 -O3
更改为 -Ofast
使此代码的运行速度几乎与 C++ 版本一样快!然而,-Ofast
改变了语言的语义——在我的测试中,它禁用了对整数溢出和数组索引溢出的检查.例如,使用 -Ofast
下面的 Swift 代码可以静默运行而不会崩溃(并打印出一些垃圾):
weather noticed that changing -O3
to -Ofast
makes this code run almost as fast as the C++ version! However, -Ofast
changes the semantics of the language a lot — in my testing, it disabled the checks for integer overflows and array indexing overflows. For example, with -Ofast
the following Swift code runs silently without crashing (and prints out some garbage):
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
用于检查(有符号的)整数溢出,而且也没有那么慢.
So -Ofast
is not what we want; the whole point of Swift is that we have the safety nets in place. Of course, the safety nets have some impact on the performance, but they should not make the programs 100 times slower. Remember that Java already checks for array bounds, and in typical cases, the slowdown is by a factor much less than 2. And in Clang and GCC we have got -ftrapv
for checking (signed) integer overflows, and it is not that slow, either.
因此问题是:我们如何在不失去安全网的情况下在 Swift 中获得合理的性能?
Hence the question: how can we get reasonable performance in Swift without losing the safety nets?
编辑 2:我做了一些更多的基准测试,使用非常简单的循环
Edit 2: I did some more benchmarking, with very simple loops along the lines of
for i in 0..<n {
x[i] = x[i] ^ 12345678
}
(这里的异或操作只是为了更容易地在汇编代码中找到相关的循环.我试图选择一个易于发现但也无害"的操作,因为它不应该需要任何与整数溢出相关的检查.)
(Here the xor operation is there just so that I can more easily find the relevant loop in the assembly code. I tried to pick an operation that is easy to spot but also "harmless" in the sense that it should not require any checks related to integer overflows.)
同样,-O3
和 -Ofast
之间的性能存在巨大差异.于是我看了一下汇编代码:
Again, there was a huge difference in the performance between -O3
and -Ofast
. So I had a look at the assembly code:
使用
-Ofast
我得到了我所期望的.相关部分是一个包含 5 条机器语言指令的循环.
With
-Ofast
I get pretty much what I would expect. The relevant part is a loop with 5 machine language instructions.
使用 -O3
我得到了超出我想象的东西.内部循环跨越 88 行汇编代码.我没有试图理解所有内容,但最可疑的部分是callq _swift_retain"的 13 次调用和另外 13 次callq _swift_release"的调用.即内循环调用26个子程序!
With -O3
I get something that was beyond my wildest imagination. The inner loop spans 88 lines of assembly code. I did not try to understand all of it, but the most suspicious parts are 13 invocations of "callq _swift_retain" and another 13 invocations of "callq _swift_release". That is, 26 subroutine calls in the inner loop!
编辑 3: 在评论中,Ferruccio 要求基准是公平的,因为它们不依赖于内置函数(例如排序).我认为以下程序是一个很好的例子:
Edit 3: In comments, Ferruccio asked for benchmarks that are fair in the sense that they do not rely on built-in functions (e.g. sort). I think the following program is a fairly good example:
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 倍:
There is no arithmetic, so we do not need to worry about integer overflows. The only thing that we do is just lots of array references. And the results are here—Swift -O3 loses by a factor almost 500 in comparison with -Ofast:
- C++ -O3:0.05 秒
- C++ -O0:0.4 秒
- Java:0.2 秒
- Python 和 PyPy:0.5 秒
- Python:12 秒
- Swift -Ofast:0.05 秒
- Swift -O3:23 秒
- Swift -O0:443 秒
(如果您担心编译器可能会完全优化无意义的循环,您可以将其更改为例如 x[i] ^= x[j]
,并添加一个打印语句,输出x[0]
.这不会改变任何东西;时间将非常相似.)
(If you are concerned that the compiler might optimize out the pointless loops entirely, you can change it to e.g. x[i] ^= x[j]
, and add a print statement that outputs x[0]
. This does not change anything; the timings will be very similar.)
是的,这里的 Python 实现是一个愚蠢的纯 Python 实现,带有一个整数列表和嵌套的 for 循环.它应该比未优化的 Swift 慢很多.Swift 和数组索引似乎严重破坏了某些东西.
And yes, here the Python implementation was a stupid pure Python implementation with a list of ints and nested for loops. It should be much slower than unoptimized Swift. Something seems to be seriously broken with Swift and array indexing.
编辑 4:这些问题(以及其他一些性能问题)似乎已在 Xcode 6 beta 5 中得到修复.
Edit 4: These issues (as well as some other performance issues) seems to have been fixed in Xcode 6 beta 5.
对于排序,我现在有以下时间:
For sorting, I now have the following timings:
- 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
产生同样好的代码.
It seems that there is no reason anymore to use the unsafe -Ofast
(a.k.a. -Ounchecked
); plain -O
produces equally good code.
推荐答案
tl;dr Swift 1.0 现在使用默认的发布优化级别 [-O] 通过此基准测试与 C 一样快.
tl;dr Swift 1.0 is now as fast as C by this benchmark using the default release optimisation level [-O].
这是 Swift Beta 中的就地快速排序:
Here is an in-place quicksort in Swift Beta:
func quicksort_swift(inout a:CInt[], start:Int, end:Int) {
if (end - start < 2){
return
}
var p = a[start + (end - start)/2]
var l = start
var r = end - 1
while (l <= r){
if (a[l] < p){
l += 1
continue
}
if (a[r] > p){
r -= 1
continue
}
var t = a[l]
a[l] = a[r]
a[r] = t
l += 1
r -= 1
}
quicksort_swift(&a, start, r + 1)
quicksort_swift(&a, r + 1, end)
}
在 C 中也一样:
void quicksort_c(int *a, int n) {
if (n < 2)
return;
int p = a[n / 2];
int *l = a;
int *r = a + n - 1;
while (l <= r) {
if (*l < p) {
l++;
continue;
}
if (*r > p) {
r--;
continue;
}
int t = *l;
*l++ = *r;
*r-- = t;
}
quicksort_c(a, r - a + 1);
quicksort_c(l, a + n - l);
}
两者都有效:
var a_swift:CInt[] = [0,5,2,8,1234,-1,2]
var a_c:CInt[] = [0,5,2,8,1234,-1,2]
quicksort_swift(&a_swift, 0, a_swift.count)
quicksort_c(&a_c, CInt(a_c.count))
// [-1, 0, 2, 2, 5, 8, 1234]
// [-1, 0, 2, 2, 5, 8, 1234]
两者都在编写的同一个程序中调用.
Both are called in the same program as written.
var x_swift = CInt[](count: n, repeatedValue: 0)
var x_c = CInt[](count: n, repeatedValue: 0)
for var i = 0; i < n; ++i {
x_swift[i] = CInt(random())
x_c[i] = CInt(random())
}
let swift_start:UInt64 = mach_absolute_time();
quicksort_swift(&x_swift, 0, x_swift.count)
let swift_stop:UInt64 = mach_absolute_time();
let c_start:UInt64 = mach_absolute_time();
quicksort_c(&x_c, CInt(x_c.count))
let c_stop:UInt64 = mach_absolute_time();
这会将绝对时间转换为秒:
This converts the absolute times to seconds:
static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MSEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MSEC;
mach_timebase_info_data_t timebase_info;
uint64_t abs_to_nanos(uint64_t abs) {
if ( timebase_info.denom == 0 ) {
(void)mach_timebase_info(&timebase_info);
}
return abs * timebase_info.numer / timebase_info.denom;
}
double abs_to_seconds(uint64_t abs) {
return abs_to_nanos(abs) / (double)NANOS_PER_SEC;
}
以下是编译器优化级别的摘要:
Here is a summary of the compiler's optimazation levels:
[-Onone] no optimizations, the default for debug.
[-O] perform optimizations, the default for release.
[-Ofast] perform optimizations and disable runtime overflow checks and runtime type checks.
[-Onone] 对于 n=10_000 的时间(以秒为单位):
Time in seconds with [-Onone] for n=10_000:
Swift: 0.895296452
C: 0.001223848
这里是 n=10_000 的 Swift 内置 sort():
Here is Swift's builtin sort() for n=10_000:
Swift_builtin: 0.77865783
这是 [-O] 对于 n=10_000:
Swift: 0.045478346
C: 0.000784666
Swift_builtin: 0.032513488
如您所见,Swift 的性能提高了 20 倍.
As you can see, Swift's performance improved by a factor of 20.
根据 mweathers 的回答,设置 [-Ofast] 产生真正的不同,导致 n=10_000 的这些时间:
As per mweathers' answer, setting [-Ofast] makes the real difference, resulting in these times for n=10_000:
Swift: 0.000706745
C: 0.000742374
Swift_builtin: 0.000603576
对于 n=1_000_000:
Swift: 0.107111846
C: 0.114957179
Swift_sort: 0.092688548
为了比较,这是与 [-Onone] 对于 n=1_000_000:
For comparison, this is with [-Onone] for n=1_000_000:
Swift: 142.659763258
C: 0.162065333
Swift_sort: 114.095478272
因此,在此基准测试中,在开发的现阶段,没有优化的 Swift 几乎比 C 慢 1000 倍.另一方面,当两个编译器都设置为 [-Ofast] 时,Swift 实际上至少与 C 一样好,如果不是比 C 稍好.
So Swift with no optimizations was almost 1000x slower than C in this benchmark, at this stage in its development. On the other hand with both compilers set to [-Ofast] Swift actually performed at least as well if not slightly better than C.
有人指出 [-Ofast] 改变了语言的语义,使其具有潜在的不安全性.这是 Apple 在 Xcode 5.0 发行说明中声明的内容:
It has been pointed out that [-Ofast] changes the semantics of the language, making it potentially unsafe. This is what Apple states in the Xcode 5.0 release notes:
新的优化级别 -Ofast,在 LLVM 中可用,可实现积极的优化.-Ofast 放宽了一些保守的限制,主要是针对浮点运算,这对大多数代码都是安全的.它可以从编译器中获得显着的高性能优势.
他们几乎都提倡它.我不能说这是否明智,但据我所知,如果您没有进行高精度浮点运算并且您确信没有整数或您的程序中可能会出现数组溢出.如果您确实需要高性能和溢出检查/精确算术,那么现在就选择另一种语言.
They all but advocate it. Whether that's wise or not I couldn't say, but from what I can tell it seems reasonable enough to use [-Ofast] in a release if you're not doing high-precision floating point arithmetic and you're confident no integer or array overflows are possible in your program. If you do need high performance and overflow checks / precise arithmetic then choose another language for now.
测试版 3 更新:
n=10_000 与 [-O]:
Swift: 0.019697268
C: 0.000718064
Swift_sort: 0.002094721
Swift 总体上要快一些,而且 Swift 的内置排序似乎发生了相当大的变化.
Swift in general is a bit faster and it looks like Swift's built-in sort has changed quite significantly.
最终更新:
[-Onone]:
Swift: 0.678056695
C: 0.000973914
[-O]:
Swift: 0.001158492
C: 0.001192406
[-未检查]:
Swift: 0.000827764
C: 0.001078914
这篇关于Swift Beta 性能:排序数组的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!