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 倍も遅くなるのは理解できません。
編集: に変更すると、このコードが C++ バージョンとほぼ同じ速度で実行される-O3
ことに気付きました。ただし、言語のセマンティクスは大幅に変更されます。私のテストでは、整数オーバーフローと配列インデックス オーバーフローのチェックが無効になりました。たとえば、次の Swift コードはクラッシュすることなく静かに実行されます (ただし、いくつかのゴミが出力されます)。-Ofast
-Ofast
-Ofast
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 行のアセンブリ コードにまたがっています。すべてを理解しようとはしませんでしたが、最も疑わしい部分は、13 回の "callq _swift_retain" の呼び出しと、さらに 13 回の "callq _swift_release" の呼び出しです。つまり、内部ループには 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秒
- C++ -O0: 0.4秒
- Java: 0.2秒
- PyPy を使用した Python: 0.5 秒
- パイソン: 12秒
- スイフト -Ofast: 0.05秒
- スイフト-O3: 23秒
- スイフト -O0: 443秒
(コンパイラが無意味なループを完全に最適化してしまうのではないかと心配な場合は、例えば に変更しx[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秒
- swiftc: 4秒
ネストされたループの場合:
- clang++ -O3: 0.06秒
- swiftc -Ofast: 0.3 秒
- swiftc -O: 0.4秒
- swiftc: 540秒
-Ofast
unsafe (別名-Ounchecked
)を使用する理由はもうないようです。plain は-O
同様に優れたコードを生成します。
ベストアンサー1
tl;dr デフォルトのリリース最適化レベル [-O] を使用したこのベンチマークでは、Swift 1.0 は C と同じくらい高速になりました。
以下は Swift 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.
n=10_000の場合の[-Onone]による秒数:
Swift: 0.895296452
C: 0.001223848
以下はn=10_000に対する Swift の組み込み 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] は言語のセマンティクスを変更し、潜在的に安全でなくなる可能性があると指摘されています。これは Apple が Xcode 5.0 のリリース ノートで述べていることです。
LLVM で利用可能な新しい最適化レベル -Ofast では、積極的な最適化が可能になります。-Ofast は、主に浮動小数点演算に関する一部の保守的な制限を緩和しますが、これはほとんどのコードにとって安全です。これにより、コンパイラから大幅な高性能が得られます。
彼らはそれを推奨しています。それが賢明かどうかは私には分かりませんが、私が言えることは、高精度の浮動小数点演算を行わず、プログラムで整数または配列のオーバーフローが発生しないことを確信している場合は、リリースで [-Ofast] を使用するのが合理的であるように思われます。高性能とオーバーフロー チェック/正確な演算が必要な場合は、今のところ別の言語を選択してください。
ベータ 3 アップデート:
n=10_000 [-O]の場合:
Swift: 0.019697268
C: 0.000718064
Swift_sort: 0.002094721
Swift は全体的に少し高速化しており、Swift の組み込みソートはかなり大幅に変更されたようです。
最終更新:
[-オノネ] :
Swift: 0.678056695
C: 0.000973914
[-O] :
Swift: 0.001158492
C: 0.001192406
[-未チェック] :
Swift: 0.000827764
C: 0.001078914