Swift Beta performance: sorting arrays
我在Swift测试版中实现了一个算法,注意到性能非常差。在深入挖掘之后,我意识到其中一个瓶颈就是排序数组。相关部分如下:
1 2 3 4 5 6 7 8 | 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=sorted(x)代表整数列表)。
在swift中,如果我使用以下命令编译它,则需要6s:
1 | xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx` |
如果我用以下命令编译它,它需要多达88秒的时间:
1 | xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx` |
xcode中"release"与"debug"构建的时间安排相似。
这里怎么了?与C++相比,我可以理解一些性能损失,但与纯Python相比,它并没有减慢10倍。
编辑:天气注意到,将EDCOX1的0个变量改为EDCOX1(1),使得这个代码运行的速度几乎和C++版本一样快!然而,
1 2 3 4 | let n = 10000000 print(n*n*n*n*n) let x = [Int](repeating: 10, count: n) print(x[n]) |
因此,我们不希望以东十一〔一〕号;斯威夫特的关键是我们有安全网。当然,安全网对性能有一定的影响,但不能使程序慢100倍。请记住,Java已经检查数组的界限,并且在典型的情况下,增长速度小于2。在clang和gcc中,我们得到了用于检查(有符号)整数溢出的
因此,问题是:我们如何在不丢失安全网的情况下,迅速获得合理的性能?
编辑2:我做了更多的基准测试,沿着
1 2 3 | for i in 0..<n { x[i] = x[i] ^ 12345678 } |
(这里有XOR操作,这样我就可以更容易地在汇编代码中找到相关的循环。我试图选择一个容易发现但又"无害"的操作,因为它不需要任何与整数溢出相关的检查。)
同样,
有了
-Ofast ,我得到了我所期望的。相关部分是一个包含5个机器语言指令的循环。有了
-O3 ,我得到了超乎想象的东西。内部循环跨越88行汇编代码。我没有试图理解所有这些,但最可疑的部分是13次调用"callq-swift-retain"和另外13次调用"callq-swift-release"。也就是说,26个子例程在内部循环中调用!
编辑3:在评论中,Ferroccio要求基准是公平的,因为它们不依赖内置函数(例如sort)。我认为以下程序是一个相当好的例子:
1 2 3 4 5 6 7 | 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倍:
- C++O3:0.05 s
- C++O0:0.4 S
- 爪哇:0.2秒
- pypy的python:0.5秒
- Python:12秒
- 快速-OFAST:0.05秒
- SWOT-O3:23秒
- 斯威夫特-O0:443 S
(如果您担心编译器可能会完全优化无意义循环,可以将其更改为例如
是的,这里的python实现是一个愚蠢的纯python实现,有一个int列表,嵌套for循环。它应该比未经优化的swift慢得多。快速和数组索引似乎严重破坏了某些功能。
编辑4:这些问题(以及其他一些性能问题)似乎已经在Xcode6测试版5中得到了解决。
对于排序,我现在有以下时间安排:
- clang++-o3:0.06秒
- Swiftc-OFast:0.1秒
- 开关柜-O:0.1s
- SWIFTC:4秒
对于嵌套循环:
- clang++-o3:0.06秒
- Swiftc-OFast:0.3秒
- 开关柜-O:0.4s
- SWIFTC:540秒
似乎没有理由再使用不安全的
TL;DrSwift1.0现在使用默认版本优化级别的基准测试速度与C一样快。
这里有一个在swift beta中的就地快速排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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中相同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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); } |
两种工作:
1 2 3 4 5 6 7 8 | 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] |
这两个都是在编写的同一个程序中调用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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(); |
这将绝对时间转换为秒:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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; } |
以下是编译器优化级别的摘要:
1 2 3 | [-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_的时间为[-onone]。
1 2 | Swift: 0.895296452 C: 0.001223848 |
以下是Swift的内置排序(),用于n=10_:
1 | Swift_builtin: 0.77865783 |
这里是用于n=10_000的[-o]
1 2 3 | Swift: 0.045478346 C: 0.000784666 Swift_builtin: 0.032513488 |
如你所见,斯威夫特的表现提高了20倍。
根据mWeathers的回答,设置[-ofast]会产生真正的差异,导致n=10_000的这些时间:
1 2 3 | Swift: 0.000706745 C: 0.000742374 Swift_builtin: 0.000603576 |
n=1_时:
1 2 3 | Swift: 0.107111846 C: 0.114957179 Swift_sort: 0.092688548 |
比较而言,n=1_时,这与[-onone]是一致的:
1 2 3 | Swift: 142.659763258 C: 0.162065333 Swift_sort: 114.095478272 |
所以在这个基准测试中,在开发的这个阶段,没有优化的Swift比C慢了近1000倍。另一方面,当两个编译器都设置为[-ofast]时,如果不是略好于c,那么swift实际上至少也执行了相同的操作。
有人指出,[-ofast]会改变语言的语义,使其具有潜在的不安全性。这就是苹果在Xcode5.0发行说明中所说的:
A new optimization level -Ofast, available in LLVM, enables aggressive optimizations. -Ofast relaxes some conservative restrictions, mostly for floating-point operations, that are safe for most code. It can yield significant high-performance wins from the compiler.
他们几乎都鼓吹它。不管这是明智还是不明智,我都不能说,但是从我所知道的来看,在一个版本中使用[-ofast]似乎足够合理,如果您不执行高精度浮点运算,并且您确信在您的程序中不可能有整数或数组溢出。如果您确实需要高性能和溢出检查/精确算术,那么现在就选择另一种语言。
贝塔3更新:
n=10_,带[-o]:
1 2 3 | Swift: 0.019697268 C: 0.000718064 Swift_sort: 0.002094721 |
一般来说,斯威夫特的速度有点快,看起来斯威夫特的内置类型已经发生了很大的变化。
最后更新:
[OnOne ]:
1 2 | Swift: 0.678056695 C: 0.000973914 |
[-O]:
1 2 | Swift: 0.001158492 C: 0.001192406 |
[-NoChest]:
1 2 | Swift: 0.000827764 C: 0.001078914 |
TL;Dr:是的,目前唯一快速的语言实现速度很慢。如果您需要快速的、数字的(和其他类型的代码,大概)代码,只需使用另一种代码。将来,你应该重新评估你的选择。不过,对于大多数在更高级别编写的应用程序代码来说,这可能已经足够好了。
从我在SIL和llvm ir中看到的情况来看,它们似乎需要一系列的优化来删除保留和发布,这些优化可能在clang(对于objective-c)中实现,但它们还没有移植它们。这就是我要遵循的理论(目前……我仍然需要确认clang对此做了些什么),因为在这个问题的最后一个测试用例上运行的分析器会产生这个"漂亮"的结果:
正如许多人所说,
当它甚至不调用可能释放对象的函数时,它不应该发出更多的保留。我不认为数组构造函数可以返回一个比要求的小的数组,这意味着许多发出的检查都是无用的。它还知道整数永远不会超过10000,因此可以优化溢出检查(不是因为
不过,编译器可能无法取消对数组或数组元素的装箱,因为它们将被传递给
这是一种非常新的(公开的)语言,我认为它正在经历很多变化,因为有人(严重地)参与了快速语言的请求反馈,他们都说语言还没有完成,将要改变。
使用代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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或Swift Runtimes的所有设备。我可能也在假设一些我没有写的东西。
为了好玩,我决定看看这个,下面是我得到的时间安排:
1 2 | Swift 4.0.2 : 0.83s (0.74s with `-Ounchecked`) C++ (Apple LLVM 8.0.0): 0.74s |
迅捷
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 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
1 2 3 4 5 6 7 | 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
1 2 3 4 5 6 7 | 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
1 2 3 4 5 6 7 | 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 |
如果我用
斯威夫特3
1 2 3 4 5 6 7 8 9 10 11 | 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 |
从Swift 2.0到Swift 3.0,似乎已经出现了性能回归,我也首次看到了
斯威夫特4
1 2 3 4 5 6 7 8 9 10 11 | 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 |
Swift 4再次提高了性能,同时保持了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #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] <<" "; cout <<"Elapsed time:" << duration_cast<duration<double>>(end - start).count() <<" "; return 0; } |
结果:
苹果叮当6
1 2 3 4 5 6 7 8 | 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
1 2 3 4 5 6 7 8 | 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
1 2 3 4 5 6 7 8 | 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
1 2 3 4 5 6 7 8 | 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
1 2 3 4 5 6 7 8 | 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 |
判决
在撰写本文时,SWIFT的排序速度很快,但还没有用EDCOX1(10)那样编译时C++的排序速度快。在EDCOX1〔9〕中,其在SWIFT 4.0.2和苹果LLVM 90.0中表现为与C+一样快。
来自
The Sort Function Swift’s standard library provides a function called
sort, which sorts an array of values of a known type, based on the
output of a sorting closure that you provide. Once it completes the
sorting process, the sort function returns a new array of the same
type and size as the old one, with its elements in the correct sorted
order.
允许您指定比较结束的默认声明:
1 | func sort<T>(array: T[], pred: (T, T) -> Bool) -> T[] |
第二个声明只接受一个参数(数组)并"硬编码以使用小于比较器"。
1 2 3 4 | func sort<T : Comparable>(array: T[]) -> T[] Example: sort( _arrayToSort_ ) { $0 > $1 } |
我在一个添加了闭包的操场上测试了一个修改过的代码版本,这样我就可以更紧密地监视函数了,我发现当n设置为1000时,闭包被调用了大约11000次。
1 2 3 4 5 6 | 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维基百科页面,并为它编写了一个快速的实现。这是我(在操场上)使用的完整程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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,我发现
似乎内置排序方法是(或接近)快速排序,并且非常缓慢…
从xcode 7开始,可以打开
重新访问Swift阵列性能:
我写了自己的基准比较swift和c/objective-c。我的基准计算质数。它使用以前质数的数组来查找每个新候选项中的质数因子,因此速度相当快。但是,它执行大量的数组读取,而对数组的写入较少。
我最初是根据Swift 1.2进行基准测试的。我决定更新这个项目,并使用Swift2.0运行它。
该项目允许您在使用普通swift数组和使用使用数组语义的swift不安全内存缓冲区之间进行选择。
对于c/objective-c,您可以选择使用nsarray,或者使用c malloc'ed数组。
测试结果似乎与最快、最小的代码优化([-0s])或最快、积极的([-0fast))优化非常相似。
关闭代码优化后,Swift2.0的性能仍然很糟糕,而C/Objective-C的性能只是稍微慢一点。
归根结底,C malloc的基于数组的计算速度最快,幅度适中。
使用最快、最小的代码优化时,带不安全缓冲区的swift比C malloc数组长1.19x-1.20x。在快速、积极的优化过程中,两者之间的差别似乎要小一些(Swift比C长1.18x到1.16x)。
如果使用常规的Swift数组,与C的差异会稍大一些。(斯威夫特需要大约1.22到1.23的时间。)
常规的Swift数组比Swift 1.2/Xcode 6中的更快。它们的性能与基于快速不安全缓冲区的数组非常接近,以至于使用不安全的内存缓冲区似乎不再值得再麻烦了,这是一个很大的问题。
顺便说一句,客观的C NSARRAY性能差。如果您要在两种语言中使用本机容器对象,那么Swift将显著加快速度。
你可以在Github的swiftPerformanceBenchmark上查看我的项目
它有一个简单的用户界面,可以很容易地收集统计数据。
有趣的是,在Swift中排序似乎比现在的C略快,但是这个素数算法在Swift中仍然更快。
其他人提到的主要问题是,
选项名随时间而变化,因此其他一些答案的构建选项具有过时的标志。正确的当前选项(Swift 2.2)是:
1 2 3 | -Onone // Debug - slow -O // Optimised -O -whole-module-optimization //Optimised across files |
整个模块优化的编译速度较慢,但可以跨模块内的文件进行优化,即在每个框架内和实际应用程序代码内进行优化,但不能在它们之间进行优化。您应该将其用于任何关键性能的应用)
您还可以禁用安全检查,以获得更高的速度,但所有断言和前提条件不仅禁用,而且在正确的基础上进行了优化。如果你有过断言,这意味着你的行为不明确。使用时要格外小心,只有当您确定提速对您有价值时(通过测试)。如果您发现它对某些代码有价值,我建议将该代码分离到一个单独的框架中,并且只禁用该模块的安全检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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示例快速排序
在对列表进行分区时,您可以查看lomuto的分区算法。用Swift写的
swift 4.1引入了新的
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/