我在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中,它需要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中“Release”和“Release”的计时。“调试”构建也类似。

这里出了什么问题?与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
}

(这里有xor操作,这样我可以更容易地在程序集代码中找到相关的循环。我试图选择一个容易发现但“无害”的操作,因为它不需要任何与整数溢出相关的检查。)

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

很快我就得到了我想要的。相关部分是一个包含5条机器语言指令的循环。 对于-O3,我得到的结果超出了我的想象。内部循环包含88行汇编代码。我没有试图理解它的全部,但最可疑的部分是“callq _swift_retain”的13次调用和“callq _swift_release”的另外13次调用。也就是说,在内部循环中有26个子例程调用!


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

let n = 10000
var x = [Int](repeating: 1, count: n)
for i in 0..<n {
    for j in 0..<n {
        x[i] = x[j]
    }
}

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

c++ -O3: 0.05 s c++ -O0: 0.4 s Java: 0.2秒 Python与PyPy: 0.5秒 Python: 12秒 Swift -Ofast: 0.05 s Swift -O3: 23秒 Swift -O0: 443 s

(如果你担心编译器可能会完全优化掉毫无意义的循环,你可以把它改为例如x[i] ^= x[j],并添加输出x[0]的print语句。这不会改变任何事情;时间将非常相似。)

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


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

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

clang++ -O3: 0.06 s swiftc -Ofast: 0.1秒 swiftc -O: 0.1秒 Swiftc: 4秒

对于嵌套循环:

clang++ -O3: 0.06 s swiftc -Ofast: 0.3秒 swiftc -O: 0.4 s 540秒

似乎没有理由再使用不安全的-Ofast(又名- unchecked);纯-O生成同样好的代码。


当前回答

Swift Array性能回顾:

我写了自己的比较Swift和C/Objective-C的基准测试。我的基准计算质数。它使用之前的素数数组来寻找每个新的候选素数中的素数因子,所以它非常快。然而,它做了大量的数组读取,而很少写入数组。

我最初是针对Swift 1.2做这个基准测试的。我决定更新项目,并在Swift 2.0上运行它。

该项目允许您选择使用普通swift数组和使用swift不安全内存缓冲区使用数组语义。

对于C/Objective-C,你可以选择使用nsarray或C malloc'ed数组。

对于最快、最小的代码优化([-0s])或最快、积极的优化([-0fast]),测试结果似乎非常相似。

在关闭代码优化后,Swift 2.0的性能仍然很糟糕,而C/Objective-C的性能只是稍微慢一些。

最重要的是,基于C malloc数组的计算是最快的

当使用最快、最小的代码优化时,Swift不安全缓冲区的时间比C malloc数组长1.19 - 1.20倍。在快速、积极的优化中,差异似乎略小(Swift比C多花1.18到1.16倍的时间。

如果使用常规Swift数组,与C语言的区别会稍微大一些。(Swift要长1.22 - 1.23)

常规的Swift数组比Swift 1.2/Xcode 6中要快得多。它们的性能非常接近Swift的基于不安全缓冲区的数组,使用不安全内存缓冲区似乎真的不值得再麻烦了,这是大的。

顺便说一句,Objective-C NSArray的性能很糟糕。如果你打算在两种语言中使用原生容器对象,Swift要快得多。

你可以在github的SwiftPerformanceBenchmark上查看我的项目

它有一个简单的UI,让收集统计数据非常容易。

有趣的是,现在在Swift中排序似乎比在C中稍微快一些,但这个质数算法在Swift中仍然更快。

其他回答

通过这个基准测试,Swift 1.0现在和C一样快,使用默认的发布优化级别[-O]。


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

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]

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

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();

这将绝对时间转换为秒:

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;
}

下面是编译器优化级别的总结:

[-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.

对于n=10_000,使用[-Onone],时间单位为秒:

Swift:            0.895296452
C:                0.001223848

下面是Swift对于n=10_000的内置sort():

Swift_builtin:    0.77865783

下面是n=10_000时的[-O]:

Swift:            0.045478346
C:                0.000784666
Swift_builtin:    0.032513488

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

根据mweathers的回答,设置[-Ofast]会产生真正的区别,导致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

为了进行比较,当n=1_000_000时,使用[-Onone]:

Swift:            142.659763258
C:                0.162065333
Swift_sort:       114.095478272

因此,在开发的这个阶段,没有优化的Swift几乎比C慢了1000倍。另一方面,当两个编译器都设置为[-Ofast] Swift时,实际上至少和C一样好,如果不是稍微好一点的话。

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

一个新的优化级别-Ofast,在LLVM中可用,支持积极的优化。ofast放宽了一些保守的限制,主要针对浮点运算,这对大多数代码都是安全的。它可以从编译器中获得显著的高性能。

他们几乎都主张这样做。我不能说这是否明智,但据我所知,如果你不做高精度浮点运算,并且你确信在你的程序中不可能有整数或数组溢出,那么在发布版中使用[-Ofast]似乎是足够合理的。如果你确实需要高性能和溢出检查/精确的算术,那么现在选择另一种语言。

Beta 3更新:

n=10_000 with [-O]:

Swift:            0.019697268
C:                0.000718064
Swift_sort:       0.002094721

Swift总体上要快一些,而且Swift的内置排序似乎有了很大的变化。

最后更新:

[-Onone]:

Swift:   0.678056695
C:       0.000973914

[o]:

Swift:   0.001158492
C:       0.001192406

[-Ounchecked]:

Swift:   0.000827764
C:       0.001078914

从Xcode 7开始,你可以打开“快速,整个模块优化”。这将立即提高您的性能。

func partition(inout list : [Int], low: Int, high : Int) -> Int {
    let pivot = list[high]
    var j = low
    var i = j - 1
    while j < high {
        if list[j] <= pivot{
            i += 1
            (list[i], list[j]) = (list[j], list[i])
        }
        j += 1
    }
    (list[i+1], list[high]) = (list[high], list[i+1])
    return i+1
}

func quikcSort(inout list : [Int] , low : Int , high : Int) {

    if low < high {
        let pIndex = partition(&list, low: low, high: high)
        quikcSort(&list, low: low, high: pIndex-1)
        quikcSort(&list, low: pIndex + 1, high: high)
    }
}

var list = [7,3,15,10,0,8,2,4]
quikcSort(&list, low: 0, high: list.count-1)

var list2 = [ 10, 0, 3, 9, 2, 14, 26, 27, 1, 5, 8, -1, 8 ]
quikcSort(&list2, low: 0, high: list2.count-1)

var list3 = [1,3,9,8,2,7,5]
quikcSort(&list3, low: 0, high: list3.count-1) 

这是我的博客关于快速排序- Github样本快速排序

您可以在partitioning the list中查看Lomuto的分区算法。用Swift编写。

Swift 4.1引入了新的-Osize优化模式。

In Swift 4.1 the compiler now supports a new optimization mode which enables dedicated optimizations to reduce code size. The Swift compiler comes with powerful optimizations. When compiling with -O the compiler tries to transform the code so that it executes with maximum performance. However, this improvement in runtime performance can sometimes come with a tradeoff of increased code size. With the new -Osize optimization mode the user has the choice to compile for minimal code size rather than for maximum speed. To enable the size optimization mode on the command line, use -Osize instead of -O.

进一步阅读:https://swift.org/blog/osize/

摘自Swift编程语言:

Swift的标准库提供了一个名为 类对已知类型的值数组进行排序 所提供的排序闭包的输出。一旦它完成 排序过程中,sort函数返回一个相同的新数组 类型和大小与旧的一样,其元素在正确的排序 秩序。

排序函数有两个声明。

允许你指定比较闭包的默认声明:

func sort<T>(array: T[], pred: (T, T) -> Bool) -> T[]

第二个声明只接受一个参数(数组),并且“硬编码以使用小于比较器”。

func sort<T : Comparable>(array: T[]) -> T[]

Example:
sort( _arrayToSort_ ) { $0 > $1 }

我在一个操场上测试了您代码的修改版本,添加了闭包,以便我可以更密切地监视函数,我发现当n设置为1000时,这个闭包被调用了大约11000次。

let n = 1000
let x = Int[](count: n, repeatedValue: 0)
for i in 0..n {
    x[i] = random()
}
let y = sort(x) { $0 > $1 }

这不是一个有效的函数,我建议使用更好的排序函数实现。

编辑:

我看了一下维基百科的Quicksort页面,并为它写了一个Swift实现。以下是我在操场上使用的完整程序

import Foundation

func quickSort(inout array: Int[], begin: Int, end: Int) {
    if (begin < end) {
        let p = partition(&array, begin, end)
        quickSort(&array, begin, p - 1)
        quickSort(&array, p + 1, end)
    }
}

func partition(inout array: Int[], left: Int, right: Int) -> Int {
    let numElements = right - left + 1
    let pivotIndex = left + numElements / 2
    let pivotValue = array[pivotIndex]
    swap(&array[pivotIndex], &array[right])
    var storeIndex = left
    for i in left..right {
        let a = 1 // <- Used to see how many comparisons are made
        if array[i] <= pivotValue {
            swap(&array[i], &array[storeIndex])
            storeIndex++
        }
    }
    swap(&array[storeIndex], &array[right]) // Move pivot to its final place
    return storeIndex
}

let n = 1000
var x = Int[](count: n, repeatedValue: 0)
for i in 0..n {
    x[i] = Int(arc4random())
}

quickSort(&x, 0, x.count - 1) // <- Does the sorting

for i in 0..n {
    x[i] // <- Used by the playground to display the results
}

当n=1000时,我发现

quickSort()被调用了大约650次, 大约进行了6000次互换, 大约有10,000个比较

似乎内置的排序方法是(或接近)快速排序,并真的很慢…