Swiftでパフォーマンスが重要なコードをいくつか書いています。考えうるすべての最適化を実装し、Instrumentsでアプリケーションをプロファイリングした後、CPUサイクルの大部分がmap()
およびreduce()
操作の実行に費やされていることに気付きました。フロート。それで、何が起こるかを見るために、map
とreduce
のすべてのインスタンスを古き良きfor
ループに置き換えました。そして驚いたことに... for
ループははるかに高速でした!
これに少し戸惑い、いくつかの大まかなベンチマークを実行することにしました。あるテストでは、次のような簡単な演算を実行した後、map
がFloatsの配列を返しました。
// Populate array with 1,000,000,000 random numbers
var array = [Float](count: 1_000_000_000, repeatedValue: 0)
for i in 0..<array.count {
array[i] = Float(random())
}
let start = NSDate()
// Construct a new array, with each element from the original multiplied by 5
let output = array.map({ (element) -> Float in
return element * 5
})
// Log the elapsed time
let elapsed = NSDate().timeIntervalSinceDate(start)
print(elapsed)
そして、同等のfor
ループ実装:
var output = [Float]()
for element in array {
output.append(element * 5)
}
map
の平均実行時間:20.1秒。 for
ループの平均実行時間:11.2秒。結果は、Floatsの代わりにIntegersを使用しても同様でした。
同様のベンチマークを作成して、Swiftのreduce
のパフォーマンスをテストしました。今回は、reduce
ループとfor
ループは、1つの大きな配列の要素を合計するときにほぼ同じパフォーマンスを達成しました。しかし、次のようにテストを100,000回ループすると:
// Populate array with 1,000,000 random numbers
var array = [Float](count: 1_000_000, repeatedValue: 0)
for i in 0..<array.count {
array[i] = Float(random())
}
let start = NSDate()
// Perform operation 100,000 times
for _ in 0..<100_000 {
let sum = array.reduce(0, combine: {$0 + $1})
}
// Log the elapsed time
let elapsed = NSDate().timeIntervalSinceDate(start)
print(elapsed)
対:
for _ in 0..<100_000 {
var sum: Float = 0
for element in array {
sum += element
}
}
reduce
メソッドは29秒かかり、for
ループは(明らかに)0.000003秒かかります。
当然、コンパイラーの最適化の結果としてその最後のテストを無視する準備ができていますが、ループとSwiftの組み込み配列メソッドに対してコンパイラーが異なる方法で最適化する方法についての洞察を与えると思います。すべてのテストは、2.5 GHz i7 MacBook Proで-Os最適化を使用して実行されたことに注意してください。結果は配列のサイズと反復回数によって異なりますが、for
ループは常に他のメソッドよりも少なくとも1.5倍、場合によっては10倍まで性能が向上しました。
ここで、Swiftのパフォーマンスについて少し困惑しています。組み込みのArrayメソッドは、このような操作を実行するための単純なアプローチよりも高速ではありませんか?たぶん、私が状況についていくらかの光を当てることができるよりも低いレベルの知識を持つ誰か。
組み込みのArrayメソッドは、このような操作を実行するための単純なアプローチよりも高速ではありませんか?たぶん、私が状況についていくらかの光を当てることができるよりも低いレベルの知識を持つ誰か。
私は、質問のこの部分と、概念レベルからさらに対処することを試みます(私の側ではSwiftのオプティマイザーの性質をほとんど理解していません)。これは、Swiftのオプティマイザーの性質に関する根深い知識よりも、コンパイラー設計とコンピューター・アーキテクチャーの背景から来ています。
呼び出しのオーバーヘッド
入力として関数を受け入れるmap
やreduce
などの関数を使用すると、オプティマイザーに大きな負担がかかり、一方通行になります。このような場合、非常に積極的な最適化に欠ける自然な誘惑は、たとえばmap
の実装と、提供されたクロージャーの間で絶えず前後に分岐し、同様にこれらの異なるコード分岐間でデータを送信することです(通常、レジスタおよびスタックを介して)。
この種の分岐/呼び出しのオーバーヘッドは、特にSwiftのクロージャーの柔軟性(不可能ではないが、概念的には非常に難しい)を考えると、オプティマイザーが除去するのは非常に困難です。 C++オプティマイザーは関数オブジェクト呼び出しをインライン化できますが、コンパイラーが渡す関数オブジェクトのタイプごとにmap
のまったく新しい命令セットを効率的に生成する必要がある場合、それを行うために必要なはるかに多くの制限とコード生成テクニックが必要です(そして、コード生成に使用される関数テンプレートを示すプログラマーの明示的な助けを借りて)。
そのため、手巻きのループがより高速に実行できることを発見しても、それほど驚くことではありません。オプティマイザにかかる負担が大幅に軽減されます。ベンダーがループを並列化するなどのことができるようになった結果、これらの高階関数が高速化できるはずだと言う人もいますが、ループを効果的に並列化するには、通常、通常の種類の情報が必要です最適化プログラムが、ネストされた関数呼び出しをインライン化して、手巻きのループと同じくらい安くなるようにします。それ以外の場合、渡す関数/クロージャーの実装はmap/reduce
のような関数に対して事実上不透明になります。呼び出すだけでオーバーヘッドを支払うことができ、並列化できません。その際の副作用とスレッドセーフ。
もちろん、これはすべて概念的です-Swiftは将来これらのケースを最適化できるかもしれません、または既にそれを今すぐにできるかもしれません(一般的に-Ofast
を参照してください- Swiftある程度の安全性を犠牲にして高速化)を行う方法を引用しました。しかし、少なくとも、これらの種類の関数を手で使用する場合、オプティマイザにより大きな負担がかかります。最初のベンチマークで見られる時間差は、この追加の呼び出しオーバーヘッドで予想される種類の違いを反映しているように見えます。アセンブリを調べてさまざまな最適化フラグを試すのが最善の方法です。
標準機能
それはそのような機能の使用を思いとどまらせることではない。彼らは意図をより簡潔に表現し、生産性を高めることができます。そして、それらに依存することで、コードベースはSwift=の将来のバージョンであなたの関与なしでより速くなる可能性があります。しかし、必ずしもより速くなるとは限りません-それは良い一般的ですあなたがしたいことをより直接的に表現する高レベルのライブラリ関数はより高速になると考えますが、ルールには常に例外がありますここで不信よりも信頼の側で)。
人工ベンチマーク
2番目のベンチマークについては、ほぼ確実に、コンパイラーがユーザー出力に影響する副作用のない離れたコードを最適化した結果です。人工ベンチマークは、無関係な副作用(本質的にユーザー出力に影響しない副作用)を排除するためにオプティマイザーが行うことの結果として、悪名高い誤解を招く傾向があります。そのため、実際にベンチマークしたいすべての作業をスキップするオプティマイザーの結果ではないため、あまりにも良いと思われる時間でベンチマークを構築するときは注意する必要があります。少なくとも、テストで計算から収集した最終結果を出力する必要があります。
最初のテスト(ループ内のmap()
vs append()
)についてはあまり言えませんが、結果を確認できます。追加すると、追加ループはさらに高速になります
_output.reserveCapacity(array.count)
_
アレイ作成後。ここではAppleで改善できる可能性があるため、バグレポートを提出してください。
に
_for _ in 0..<100_000 {
var sum: Float = 0
for element in array {
sum += element
}
}
_
計算結果はまったく使用されないため、コンパイラは(おそらく)ループ全体を削除します。同様の最適化が行われない理由を推測することしかできません
_for _ in 0..<100_000 {
let sum = array.reduce(0, combine: {$0 + $1})
}
_
しかし、クロージャーを使用してreduce()
を呼び出すと副作用があるかどうかを判断するのはより困難です。
およびprint合計を計算するためにテストコードがわずかに変更された場合
_do {
var total = Float(0.0)
let start = NSDate()
for _ in 0..<100_000 {
total += array.reduce(0, combine: {$0 + $1})
}
let elapsed = NSDate().timeIntervalSinceDate(start)
print("sum with reduce:", elapsed)
print(total)
}
do {
var total = Float(0.0)
let start = NSDate()
for _ in 0..<100_000 {
var sum = Float(0.0)
for element in array {
sum += element
}
total += sum
}
let elapsed = NSDate().timeIntervalSinceDate(start)
print("sum with loop:", elapsed)
print(total)
}
_
私のテストでは両方のバリアントが約10秒かかります。
文字列の配列で繰り返される変換のパフォーマンスを測定するパフォーマンステストのクイックセットを実行したところ、.map
は、forループよりも約10倍のパフォーマンスでした。
以下のスクリーンショットの結果は、単一のmap
ブロック内の連鎖変換が、それぞれに単一の変換を持つ複数のmap
sよりも優れていること、およびmap
の使用がループに対して優れていることを示しています。
プレイグラウンドで使用したコード:
import Foundation
import XCTest
class MapPerfTests: XCTestCase {
var array =
[
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString",
"MyString"
]
func testForLoopAllInOnePerf() {
measure {
var newArray: [String] = []
for item in array {
newArray.append(item.uppercased().lowercased().uppercased().lowercased())
}
}
}
func testForLoopMultipleStagesPerf() {
measure {
var newArray: [String] = []
for item in array {
let t1 = item.uppercased()
let t2 = item.lowercased()
let t3 = item.uppercased()
let t4 = item.lowercased()
newArray.append(t4)
}
}
}
func testMultipleMapPerf() {
measure {
let newArray = array
.map( { $0.uppercased() } )
.map( { $0.lowercased() } )
.map( { $0.uppercased() } )
.map( { $0.lowercased() } )
}
}
func testSingleMapPerf() {
measure {
let newArray = array
.map( { $0.uppercased().lowercased().uppercased().lowercased() } )
}
}
}
MapPerfTests.defaultTestSuite.run()