Swift ベータ版のパフォーマンス: 配列のソート 質問する

Swift ベータ版のパフォーマンス: 配列のソート 質問する

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秒

-Ofastunsafe (別名-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

おすすめ記事