斯威夫特测试版性能:数组排序

问题描述 投票:899回答:9

我正在执行在斯威夫特测试版的算法,并发现表现得非常差。更深的挖掘后,我意识到的瓶颈之一是作为排序数组一样简单。相关部分是在这里:

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 =分类(X)为整数的列表)。

在斯威夫特需要6秒,如果我用下面的命令编译:

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是不是我们想要的;斯威夫特的整点是,我们已经制定了安全网。当然,安全网对性能有一些影响,但他们不应该让程序慢100倍。请记住,Java的已检查数组边界,并在典型情况下,放缓的一个因素比2.而在锵和GCC少得多,我们已经得到-ftrapv检查(签字)整数溢出,这是不慢,无论是。

因此,这个问题:我们如何能够得到合理的斯威夫特表现不失安全网?


编辑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]
    }
}

没有算术,所以我们并不需要担心整数溢出。我们唯一要做的就是大量的数组引用的。其结果是在这里,斯威夫特-O3近500与-Ofast比较失去的一个因素:

  • C ++ -O3:0.05秒
  • C ++ -O0:0.4秒
  • Java的:0.2秒
  • Python和PyPy:0.5秒
  • Pothon:12
  • 斯威夫特-Ofast:0.05秒
  • 斯威夫特-O3:23号
  • 斯威夫特-O0:443个小号

(如果您担心编译器可能完全优化了毫无意义的循环,你可以把它改成例如x[i] ^= x[j],并添加输出x[0]打印声明这不会改变任何东西;定时将是非常相似的。)

是的,这里的Python实现是一个愚蠢的纯Python实现与整数列表和嵌套的for循环。它应该比未优化雨燕慢得多。事情似乎与斯威夫特和数组索引被严重损坏。


编辑4:这些问题(以及其他一些性能问题)似乎已经固定在Xcode 6测试版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(又名-Ounchecked);平原-O产生同样良好的代码。

swift performance sorting xcode6 compiler-optimization
9个回答
452
投票

TL;博士夫特1.0现在一样快,C利用默认释放优化级别[-O]这个基准。


下面是就地快速排序在斯威夫特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]

这两个被称为在同一个程序的编写。

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.

时间与[-Onone]对于n = 10_000秒:

Swift:            0.895296452
C:                0.001223848

这是斯威夫特的内置排序()对于n = 10_000:

Swift_builtin:    0.77865783

下面是[-O]对于n = 10_000:

Swift:            0.045478346
C:                0.000784666
Swift_builtin:    0.032513488

正如你所看到的,斯威夫特的表现由20倍提高。

按照mweathers' answer,设置[-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

为了进行比较,这是与[-Onone]对于n = 1_000_000:

Swift:            142.659763258
C:                0.162065333
Swift_sort:       114.095478272

如此迅速,没有优化几乎慢1000倍,比C在这个基准测试,在这个阶段的发展。在另一方面用两个编译器设置为[-Ofast]夫特实际进行至少,以及如果不大于C.稍好

据指出,[-Ofast]改变语言的语义,使其成为潜在的不安全。这是苹果公司在Xcode 5.0版本说明指出:

一种新的优化级别-Ofast,在LLVM提供,能够主动优化。 -Ofast放松一些保守的限制,主要用于浮点运算,这对于大多数的代码是安全的。它可以从编译器产生显著高性能胜。

他们都主张,但它。这是否是明智与否我不能说,但我可以告诉它似乎不够合理的,如果你没有做高精度浮点运算使用[-Ofast]在一份新闻稿中,你有信心没有整数或阵列溢出在你的程序是可能的。如果您确实需要高性能和溢出检查/精确运算,然后选择另一种语言现在。

BETA 3日更新:

N = 10_000与[-O]:

Swift:            0.019697268
C:                0.000718064
Swift_sort:       0.002094721

斯威夫特一般是快一点,它看起来像雨燕内置的排序已经相当显著改变。

最后更新:

[-在一个]:

Swift:   0.678056695
C:       0.000973914

[O]:

Swift:   0.001158492
C:       0.001192406

[-Ounchecked]:

Swift:   0.000827764
C:       0.001078914

107
投票

TL; DR:是的,只有雨燕语言的实现是缓慢的,就是现在。如果您需要快速,数字(和其他类型的代码,想必)代码,只是去另一个。在未来,你应该重新评估你的选择。这可能是是在更高层次上编写大部分应用代码不够好,虽然。

从我所看到的在SIL和LLVM IR,好像他们需要一堆优化去除保留和释放,这可能会在Clang来实现(Objective-C的),但他们并没有移植他们没有。这是理论,我有去(现在...我还需要确认锵做一些事情),因为在这个问题上的最后一个测试案例剖析器运行产生了这个“漂亮”的结果:

正如许多人说,-Ofast完全是不安全的,改变语言的语义。对我来说,这是在“如果你要使用,只是用另一种语言”的阶段。我将在稍后重新评估这一选择,如果它的变化。

-O3会让我们一堆swift_retainswift_release调用,说实话,并不像他们应该为这个例子在那里。优化应该省略掉他们(大部分)AFAICT,因为它知道大多数关于阵列的信息,并且知道它有(至少)一个强有力的参考吧。

它不应该发出更多的保留时,它甚至没有叫这可能会释放对象的功能。我不认为一个数组构造函数可以返回一个数组比什么要求,这意味着大量的中发出的检查是无用的小。它也知道整数永远不会10K以上,所以溢出检查可以优化(不-Ofast古怪的,而是因为语言的语义(没有做任何改变这种变种也可以访问它,加起来的10K是该类型Int安全)。

编译器可能无法拆箱数组或数组元素,不过,因为他们要传递给sort(),这是一个外部函数,并要得到它的预期的参数。这将使我们不得不间接地使用Int值,这将使它走慢一点。如果sort()泛型函数(而不是在多路方法)是提供给编译器,结果被内联这可能会改变。

这是一个非常新的(公开)的语言,它正在经历什么,我认为有很多的变化,因为其中会涉及与雨燕语言的人(严重)寻求反馈,他们都表示语言未完成和意志更改。

代码中使用:

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")

P.S:我不是Objective-C的,也没有从Cocoa所有设施,Objective-C中,或运行时间雨燕的专家。我也可能被假定一些事情,我没有写。


50
投票

我决定去看看这个乐趣,这里是我得到的时机:

Swift 4.0.2           :   0.83s (0.74s with `-Ounchecked`)
C++ (Apple LLVM 8.0.0):   0.74s

迅速

// Swift 4.0 code
import Foundation

func doTest() -> Void {
    let arraySize = 10000000
    var randomNumbers = [UInt32]()

    for _ in 0..<arraySize {
        randomNumbers.append(arc4random_uniform(UInt32(arraySize)))
    }

    let start = Date()
    randomNumbers.sort()
    let end = Date()

    print(randomNumbers[0])
    print("Elapsed time: \(end.timeIntervalSince(start))")
}

doTest()

结果:

雨燕1.1

xcrun swiftc --version
Swift version 1.1 (swift-600.0.54.20)
Target: x86_64-apple-darwin14.0.0

xcrun swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 1.02204304933548

雨燕1.2

xcrun swiftc --version
Apple Swift version 1.2 (swiftlang-602.0.49.6 clang-602.0.49)
Target: x86_64-apple-darwin14.3.0

xcrun -sdk macosx swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 0.738763988018036

雨燕2.0

xcrun swiftc --version
Apple Swift version 2.0 (swiftlang-700.0.59 clang-700.0.72)
Target: x86_64-apple-darwin15.0.0

xcrun -sdk macosx swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 0.767306983470917

看来如果我-Ounchecked编译是相同的性能。

雨燕3.0

xcrun swiftc --version
Apple Swift version 3.0 (swiftlang-800.0.46.2 clang-800.0.38)
Target: x86_64-apple-macosx10.9

xcrun -sdk macosx swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 0.939633965492249

xcrun -sdk macosx swiftc -Ounchecked SwiftSort.swift
./SwiftSort     
Elapsed time: 0.866258025169373

似乎是一个性能回归从雨燕2.0至3.0雨燕,而且我也看到首次-O-Ounchecked之间的差异。

雨燕4.0

xcrun swiftc --version
Apple Swift version 4.0.2 (swiftlang-900.0.69.2 clang-900.0.38)
Target: x86_64-apple-macosx10.9

xcrun -sdk macosx swiftc -O SwiftSort.swift
./SwiftSort     
Elapsed time: 0.834299981594086

xcrun -sdk macosx swiftc -Ounchecked SwiftSort.swift
./SwiftSort     
Elapsed time: 0.742045998573303

斯威夫特再次4提高性能,同时保持-O-Ounchecked之间的间隙。 -O -whole-module-optimization没有出现有所作为。

C ++

#include <chrono>
#include <iostream>
#include <vector>
#include <cstdint>
#include <stdlib.h>

using namespace std;
using namespace std::chrono;

int main(int argc, const char * argv[]) {
    const auto arraySize = 10000000;
    vector<uint32_t> randomNumbers;

    for (int i = 0; i < arraySize; ++i) {
        randomNumbers.emplace_back(arc4random_uniform(arraySize));
    }

    const auto start = high_resolution_clock::now();
    sort(begin(randomNumbers), end(randomNumbers));
    const auto end = high_resolution_clock::now();

    cout << randomNumbers[0] << "\n";
    cout << "Elapsed time: " << duration_cast<duration<double>>(end - start).count() << "\n";

    return 0;
}

结果:

苹果锵6.0

clang++ --version
Apple LLVM version 6.0 (clang-600.0.54) (based on LLVM 3.5svn)
Target: x86_64-apple-darwin14.0.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.688969

苹果锵6.1.0

clang++ --version
Apple LLVM version 6.1.0 (clang-602.0.49) (based on LLVM 3.6.0svn)
Target: x86_64-apple-darwin14.3.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.670652

苹果锵7.0.0

clang++ --version
Apple LLVM version 7.0.0 (clang-700.0.72)
Target: x86_64-apple-darwin15.0.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.690152

苹果锵8.0.0

clang++ --version
Apple LLVM version 8.0.0 (clang-800.0.38)
Target: x86_64-apple-darwin15.6.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.68253

苹果锵9.0.0

clang++ --version
Apple LLVM version 9.0.0 (clang-900.0.38)
Target: x86_64-apple-darwin16.7.0
Thread model: posix

clang++ -O3 -std=c++11 CppSort.cpp -o CppSort
./CppSort     
Elapsed time: 0.736784

判决书

在编写这本书的时候,斯威夫特的排序是快,但没有一样快,当-O编,与上述的编译器和库C ++的排序。随着-Ounchecked,这似乎是一样快,C ++在斯威夫特4.0.2和苹果LLVM 9.0.0。


33
投票

The Swift Programming Language

排序函数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 }

这不是一个有效的功能,我会建议使用较好的排序功能的实现。

编辑:

我接过来一看,在快速排序维基百科页面,并写了一个迅速执行它。下面是完整的程序我用(在操场)

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利用这一点,我发现,

  1. 快速排序()接到电话约650倍,
  2. 约6000互换进行了改造,
  3. 并有大约10,000比较

似乎内置的排序方法是(或接近)快速排序,而实在是太慢了...


18
投票

由于Xcode中7,你可以打开Fast, Whole Module Optimization。这应该立即提高你的表现。


11
投票

斯威夫特阵列性能回顾:

我写我自己的基准与C / Objective-C的比较斯威夫特。我的基准计算质数。它使用以前的素数的阵列,以寻找在每个新的候选素因子,所以它是相当快的。然而,它确实阵列读取的吨,和较少写入阵列。

本来我这个基准对雨燕1.2。我决定更新项目并运行它针对雨燕2.0。

该项目可以让你使用普通迅速阵列和使用使用数组语义斯威夫特不安全的内存缓冲区之间进行选择。

对于C / Objective-C中,您可以选择使用NSArrays或C malloc分配阵列。

测试结果似乎与最快的速度,最小的代码优化([-0s])或最快,攻击性([-0fast])优化非常相似。

迅速2.0性能仍然可怕与代码优化断开,而C / Objective-C的性能仅适度慢。

底线是,C malloc分配基于阵列的计算是最快的,通过适度的余量

斯威夫特与不安全的缓冲区需要大约1.19X - 用最快的速度,最小的代码优化时,比C malloc分配阵列1.20X更长。差别似乎与快,狠优化略少(斯威夫特需要更多的像1.18x至1.16x比C长

如果使用常规的斯威夫特阵列,与C不同的是稍大。 (SWIFT花费〜1.22至1.23更长的时间。)

定期斯威夫特阵列DRAMATICALLY速度比他们在雨燕1.2 / Xcode中6.他们的表现是如此接近雨燕不安全的缓冲基于阵列,使用不安全的内存缓冲区并没有真正似乎值得的麻烦更多,这是很大的。

BTW,Objective-C的NSArray的表现太臭。如果你要使用本机的容器对象在两种语言中,斯威夫特是大大加快。

您可以在SwiftPerformanceBenchmark在github上看看我的项目

它有一个简单的用户界面,使得收集的统计很简单。

有趣的是,排序似乎略快于比C现在斯威夫特,但这一素数的算法仍然较快斯威夫特。


8
投票

由别人提及,但不是叫出来的不足主要问题是,-O3什么事情都不在斯威夫特(和从来没有),所以当与编译它是有效的非优化(-Onone)。

选项名称随时间的变化等等一些其他的答案有构建选项过时的标志。正确的电流选项(雨燕2.2)是:

-Onone // Debug - slow
-O     // Optimised
-O -whole-module-optimization //Optimised across files

整个模块优化具有较慢的编译但可以在每个框架内并在实际的应用程序代码内,但不在它们之间优化跨越内的模块,即文件。你应该用这个东西性能的关键)

您也可以禁用安全检查,甚至更多的速度,但与不只是停止,但依据它们是正确的上优化了所有的断言和前提条件。如果你打的断言,这意味着你进入未定义行为。请务必谨慎使用且仅当你确定的速度提升是值得你(通过测试)。如果你发现它的一些代码,我建议分开的代码到一个单独的框架,只是停用该模块的安全检查是有价值的。


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 sample Quick-Sort博客

你可以看看关于分区列表Lomuto的分区算法。写在斯威夫特


4
投票

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

在雨燕4.1的编译器现在支持新的优化模式,支持专用优化,从而降低代码大小。

雨燕的编译器都带有强大的优化。当与-O编译,编译器试图转换代码,以便它最大的效能执行。但是,这种改善在运行时的性能有时会增加代码大小的权衡。随着新-Osize优化模式得到了用户的选择来编译最少的代码大小,而不是最大速度。

为了使在命令行上的尺寸优化模式中,使用-Osize代替-O。

延伸阅读:https://swift.org/blog/osize/

© www.soinside.com 2019 - 2024. All rights reserved.