Swift Beta でアルゴリズムを実装していて、パフォーマンスが非常に悪いことに気付きました。深く掘り下げた後、ボトルネックの 1 つは配列の並べ替えと同じくらい単純なものであることに気付きました。関連する部分は次のとおりです。
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 では、次のコマンドでコンパイルすると6 秒かかります。
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
次のコマンドでコンパイルすると、88 秒もかかります。
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
たちが望んでいることではありません。Swift の要点は、セーフティ ネットが整っていることです。もちろん、セーフティ ネットはパフォーマンスにある程度の影響を与えますが、プログラムを 100 倍遅くするべきではありません。Java はすでに配列の境界をチェックしていることを思い出してください。典型的なケースでは、速度低下は 2 倍未満です。また、Clang と GCC-ftrapv
では、(符号付き) 整数のオーバーフローをチェックしていますが、それほど遅くはありません。
したがって、質問: セーフティ ネットを失うことなく、Swift で妥当なパフォーマンスを得るにはどうすればよいでしょうか?
編集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:コメントで、Ferruccio は、組み込み関数 (並べ替えなど) に依存しないという意味で公正なベンチマークを求めました。次のプログラムはかなり良い例だと思います。
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 秒
- C++ -O0: 0.4 秒
- Java: 0.2 秒
- Python と PyPy: 0.5 秒
- Python: 12 秒
- Swift -Ofast: 0.05 秒
- スイフト -O3: 23 秒
- スイフト -O0: 443 秒
(コンパイラが無意味なループを完全に最適化するのではないかと懸念している場合は、それを egx[i] ^= x[j]
に変更し、を出力する print ステートメントを追加できますx[0]
。これは何も変更しません。タイミングは非常に似ています。)
そして、はい、ここでの Python 実装は、int のリストと入れ子になった for ループを持つ愚かな純粋な Python 実装でした。最適化されていない Swift よりもはるかに遅くなるはずです。Swift と配列のインデックス付けで何かが深刻に壊れているようです。
編集 4:これらの問題 (およびその他のパフォーマンスの問題) は、Xcode 6 ベータ 5 で修正されたようです。
並べ替えについては、次のタイミングがあります。
- clang++ -O3: 0.06 秒
- swiftc -Ofast: 0.1 秒
- swiftc -O: 0.1 秒
- スイフト:4秒
ネストされたループの場合:
- clang++ -O3: 0.06 秒
- swiftc -Ofast: 0.3 秒
- swiftc -O: 0.4 秒
- スイフト:540秒
-Ofast
unsafe (別名-Ounchecked
)を使用する理由はもうないようです。plain-O
も同様に優れたコードを生成します。