我在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 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开始,你可以打开“快速,整个模块优化”。这将立即提高您的性能。

摘自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个比较

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

通过这个基准测试,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

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/

TL;DR:是的,目前唯一的Swift语言实现速度很慢。如果您需要快速的数字代码(或者其他类型的代码),那就使用另一种代码。在未来,你应该重新评估你的选择。不过,对于在更高级别上编写的大多数应用程序代码来说,这可能已经足够好了。

从我在SIL和LLVM IR中看到的情况来看,他们似乎需要一堆优化来删除保留和释放,这可能会在Clang中实现(针对Objective-C),但他们还没有移植它们。这就是我的理论(现在……我仍然需要确认Clang对此做了一些事情),因为在这个问题的最后一个测试用例上运行的分析器产生了这个“漂亮”的结果:

正如许多人所说,-Ofast是完全不安全的,并且改变了语言语义。对我来说,这是在“如果你要使用这种语言,那就使用另一种语言”的阶段。我以后会重新评估这个选择,如果它改变了。

-O3为我们提供了一堆swift_retain和swift_release调用,老实说,它们看起来不应该出现在这个例子中。优化器应该已经省略了(大部分)AFAICT,因为它知道关于数组的大部分信息,并且知道它(至少)有一个对数组的强引用。

It shouldn't emit more retains when it's not even calling functions which might release the objects. I don't think an array constructor can return an array which is smaller than what was asked for, which means that a lot of checks that were emitted are useless. It also knows that the integer will never be above 10k, so the overflow checks can be optimized (not because of -Ofast weirdness, but because of the semantics of the language (nothing else is changing that var nor can access it, and adding up to 10k is safe for the type Int).

不过,编译器可能无法解方框数组或数组元素,因为它们被传递给sort(),这是一个外部函数,必须获得它所期望的参数。这将使我们不得不间接使用Int值,这将使它运行得有点慢。如果sort()泛型函数(不是以多方法的方式)对编译器可用并内联,则这可能会改变。

这是一种非常新的(公开的)语言,我认为它正在经历很多变化,因为有很多人(大量)参与Swift语言的工作,他们都说语言还没有完成,而且会改变。

代码使用:

import Cocoa

let swift_start = NSDate.timeIntervalSinceReferenceDate();
let n: Int = 10000
let x = Int[](count: n, repeatedValue: 1)
for i in 0..n {
    for j in 0..n {
        let tmp: Int = x[j]
        x[i] = tmp
    }
}
let y: Int[] = sort(x)
let swift_stop = NSDate.timeIntervalSinceReferenceDate();

println("\(swift_stop - swift_start)s")

附注:我不是Objective-C的专家,也不是Cocoa、Objective-C或Swift运行时的所有工具的专家。我可能还假设了一些我没有写的东西。