Performances de Swift Beta: tri des tableaux

928

J'implémentais un algorithme dans Swift Beta et j'ai remarqué que les performances étaient très mauvaises. Après avoir creusé plus profondément, j'ai réalisé que l'un des goulots d'étranglement était quelque chose d'aussi simple que de trier des tableaux. La partie pertinente est ici:

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

En C ++, une opération similaire prend 0,06 s sur mon ordinateur.

En Python, cela prend 0,6 s (pas de trucs, juste y = trié (x) pour une liste d'entiers).

Dans Swift, cela prend 6 secondes si je le compile avec la commande suivante:

xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`

Et cela prend autant que 88s si je le compile avec la commande suivante:

xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`

Les timings dans Xcode avec les versions "Release" vs. "Debug" sont similaires.

Qu'est-ce qui ne va pas ici? Je pouvais comprendre une perte de performances par rapport à C ++, mais pas un ralentissement de 10 fois par rapport à Python pur.


Edit: la météo a remarqué que la modification -O3de -Ofastce code rend le code presque aussi rapide que la version C ++! Cependant, cela -Ofastchange beaucoup la sémantique du langage - dans mes tests, il a désactivé les vérifications des débordements d'entiers et les débordements d'indexation des tableaux . Par exemple, avec -Ofastle code Swift suivant s'exécute en silence sans se bloquer (et imprime des ordures):

let n = 10000000
print(n*n*n*n*n)
let x =  [Int](repeating: 10, count: n)
print(x[n])

Ce -Ofastn'est donc pas ce que nous voulons; tout l'intérêt de Swift est que nous avons mis en place les filets de sécurité. Bien sûr, les filets de sécurité ont un certain impact sur les performances, mais ils ne devraient pas ralentir les programmes 100 fois. N'oubliez pas que Java vérifie déjà les limites des tableaux, et dans des cas typiques, le ralentissement est d'un facteur bien inférieur à 2. Et dans Clang et GCC, nous avons obtenu -ftrapvpour vérifier les débordements d'entiers (signés), et ce n'est pas si lent non plus.

D'où la question: comment obtenir des performances raisonnables dans Swift sans perdre les filets de sécurité?


Edit 2: J'ai fait un peu plus de benchmarking, avec des boucles très simples le long des lignes de

for i in 0..<n {
    x[i] = x[i] ^ 12345678
}

(Ici, l'opération xor est là juste pour que je puisse trouver plus facilement la boucle appropriée dans le code d'assemblage. J'ai essayé de choisir une opération qui est facile à repérer mais aussi "inoffensive" dans le sens où elle ne devrait pas nécessiter de vérifications liées en débordements entiers.)

Encore une fois, il y avait une énorme différence dans les performances entre -O3et -Ofast. J'ai donc jeté un œil au code assembleur:

  • Avec, -Ofastj'obtiens à peu près ce à quoi je m'attendais. La partie pertinente est une boucle avec 5 instructions en langage machine.

  • Avec, -O3j'obtiens quelque chose qui dépassait mon imagination la plus folle. La boucle intérieure s'étend sur 88 lignes de code d'assemblage. Je n'ai pas essayé de comprendre tout cela, mais les parties les plus suspectes sont 13 invocations de "callq _swift_retain" et 13 autres invocations de "callq _swift_release". Autrement dit, 26 appels de sous-programme dans la boucle interne !


Edit 3: Dans les commentaires, Ferruccio a demandé des repères qui sont justes en ce sens qu'ils ne reposent pas sur des fonctions intégrées (par exemple, le tri). Je pense que le programme suivant est un assez bon exemple:

let n = 10000
var x = [Int](repeating: 1, count: n)
for i in 0..<n {
    for j in 0..<n {
        x[i] = x[j]
    }
}

Il n'y a pas d'arithmétique, nous n'avons donc pas à nous soucier des débordements d'entiers. La seule chose que nous faisons est juste de nombreuses références de tableaux. Et les résultats sont là - Swift -O3 perd par un facteur près de 500 par rapport à -Ofast:

  • C ++ -O3: 0,05 s
  • C ++ -O0: 0,4 s
  • Java: 0,2 s
  • Python avec PyPy: 0,5 s
  • Python: 12 s
  • Rapide-Rapide: 0,05 s
  • Swift -O3: 23 s
  • Swift -O0: 443 s

(Si vous craignez que le compilateur optimise entièrement les boucles inutiles, vous pouvez le changer par exemple x[i] ^= x[j], et ajouter une instruction print qui sort x[0]. Cela ne change rien; les timings seront très similaires.)

Et oui, ici l'implémentation Python était une implémentation Python pure stupide avec une liste d'entiers et imbriquée pour les boucles. Il devrait être beaucoup plus lent que Swift non optimisé. Quelque chose semble sérieusement rompu avec Swift et l'indexation de tableaux.


Edit 4: Ces problèmes (ainsi que certains autres problèmes de performances) semblent avoir été corrigés dans Xcode 6 beta 5.

Pour le tri, j'ai maintenant les horaires suivants:

  • clang ++ -O3: 0,06 s
  • swiftc -Ofast: 0,1 s
  • swiftc -O: 0,1 s
  • swiftc: 4 s

Pour les boucles imbriquées:

  • clang ++ -O3: 0,06 s
  • swiftc -Ofast: 0,3 s
  • swiftc -O: 0,4 s
  • swiftc: 540 s

Il semble qu'il n'y ait plus de raison d'utiliser le dangereux -Ofast(aka -Ounchecked); plain -Oproduit un code tout aussi bon.

Jukka Suomela
la source
20
Voici une autre question "Swift 100 fois plus lent que C": stackoverflow.com/questions/24102609/…
Jukka Suomela
16
Et voici une discussion sur le matériel marketing d'Apple lié aux bonnes performances de Swift dans le tri: programmers.stackexchange.com/q/242816/913
Jukka Suomela
2
Vous pouvez compiler avec: xcrun --sdk macosx swift -O3. C'est plus court.
Southern Hospitality
3
Ce lien montre quelques autres opérations de base par rapport à Objective-C.
Wold
4
Avec la version bêta 5, la vitesse de Swift s'est considérablement améliorée - voir ce billet de Jesse Squires pour plus de détails.
Nate Cook

Réponses:

460

tl; dr Swift 1.0 est maintenant aussi rapide que C par ce benchmark en utilisant le niveau d'optimisation de version par défaut [-O].


Voici un tri rapide sur place dans 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)
}

Et la même chose en 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);
}

Les deux fonctionnent:

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]

Les deux sont appelés dans le même programme que celui écrit.

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();

Cela convertit les temps absolus en secondes:

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;
}

Voici un résumé des niveaux d'optimisation du compilateur:

[-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.

Temps en secondes avec [-Onone] pour n = 10_000 :

Swift:            0.895296452
C:                0.001223848

Voici le tri intégré de Swift () pour n = 10_000 :

Swift_builtin:    0.77865783

Voici [-O] pour n = 10_000 :

Swift:            0.045478346
C:                0.000784666
Swift_builtin:    0.032513488

Comme vous pouvez le voir, les performances de Swift se sont améliorées d'un facteur 20.

Selon la réponse de mweathers , le réglage de [-Ofast] fait la vraie différence, résultant en ces temps pour n = 10_000 :

Swift:            0.000706745
C:                0.000742374
Swift_builtin:    0.000603576

Et pour n = 1_000_000 :

Swift:            0.107111846
C:                0.114957179
Swift_sort:       0.092688548

A titre de comparaison, c'est avec [-Onone] pour n = 1_000_000 :

Swift:            142.659763258
C:                0.162065333
Swift_sort:       114.095478272

Swift sans optimisation était donc presque 1000 fois plus lent que C dans cette référence, à ce stade de son développement. D'un autre côté, avec les deux compilateurs réglés sur [-Ofast], Swift a en fait réalisé au moins aussi bien sinon légèrement mieux que C.

Il a été souligné que [-Ofast] modifie la sémantique du langage, le rendant potentiellement dangereux. Voici ce qu'Apple déclare dans les notes de publication de Xcode 5.0:

Un nouveau niveau d'optimisation -Ofast, disponible dans LLVM, permet des optimisations agressives. -Ofast assouplit certaines restrictions conservatrices, principalement pour les opérations en virgule flottante, qui sont sûres pour la plupart du code. Il peut générer des gains de performances importants du compilateur.

Ils le préconisent tous. Que ce soit sage ou non, je ne pourrais pas le dire, mais d'après ce que je peux dire, il semble assez raisonnable d'utiliser [-Ofast] dans une version si vous ne faites pas de l'arithmétique à virgule flottante de haute précision et que vous êtes sûr qu'aucun entier ou les débordements de tableau sont possibles dans votre programme. Si vous avez besoin de vérifications de haute performance et de dépassement / arithmétique précise, choisissez une autre langue pour l'instant.

MISE À JOUR BETA 3:

n = 10_000 avec [-O] :

Swift:            0.019697268
C:                0.000718064
Swift_sort:       0.002094721

Swift en général est un peu plus rapide et il semble que le tri intégré de Swift ait changé de manière assez significative.

MISE À JOUR FINALE:

[-Unique] :

Swift:   0.678056695
C:       0.000973914

[-O] :

Swift:   0.001158492
C:       0.001192406

[-Ounchecked] :

Swift:   0.000827764
C:       0.001078914
Joseph Mark
la source
25
L'utilisation de -emit-sil pour sortir le code SIL intermédiaire montre ce qui est conservé (argh, le débordement de pile rend cela impossible à formater). Il s'agit d'un objet tampon interne dans le tableau. Cela ressemble définitivement à un bogue d'optimiseur, l'optimiseur ARC devrait pouvoir supprimer les retenues sans -Ofast.
Catfish_Man
'll juste en désaccord que nous devons utiliser une autre langue si vous souhaitez utiliser les optimisations Ofast. Il devra traiter de la même manière la question des vérifications des limites et d'autres problèmes mineurs si vous choisissez une autre langue comme C. Le swift est cool précisément parce qu'il doit être sécurisé par défaut et éventuellement rapide et non sécurisé si nécessaire. Cela permet également au programmeur de déboguer votre code, pour vous assurer que tout va bien et compiler à l'aide d'Ofast. La possibilité d'utiliser des normes modernes tout en ayant la puissance d'un langage "dangereux" comme C est très cool.
Wallacy
2
si vous pouvez me dire comment cela pourrait être invalide, veuillez le faire. j'aime toujours en savoir plus
Joseph Mark
3
fait une mise à jour finale, Swift est maintenant aussi rapide que C par cette référence en utilisant des optimisations standard.
Joseph Mark
4
Astuce: vos implémentations Swift et C de quicksort peuvent être améliorées si vous recursez d'abord sur la plus petite partition! (Au lieu de récurser sur la partition de gauche toujours en premier.) Quicksort implémenté avec une simple sélection de pivot dans le pire des cas prend du temps O (n ^ 2), mais même dans ce pire cas, vous n'avez besoin que de l'espace de pile O (log n) en récursant sur la plus petite partition en premier.
Macneil Shonle
108

TL; DR : Oui, la seule implémentation du langage Swift est lente, en ce moment . Si vous avez besoin d'un code rapide et numérique (et d'autres types de code, vraisemblablement), choisissez-en un autre. À l'avenir, vous devriez réévaluer votre choix. Cependant, cela peut être suffisant pour la plupart des codes d'application écrits à un niveau supérieur.

D'après ce que je vois dans SIL et LLVM IR, il semble qu'ils aient besoin d'un tas d'optimisations pour supprimer les retenues et les versions, qui pourraient être implémentées dans Clang (pour Objective-C), mais ils ne les ont pas encore portées. C'est la théorie avec laquelle je vais (pour l'instant ... je dois encore confirmer que Clang fait quelque chose), puisqu'un profileur exécuté sur le dernier cas de test de cette question donne ce "joli" résultat:

Profilage temporel sur -O3 Profilage temporel sur -Ofast

Comme cela a été dit par beaucoup d'autres, -Ofastest totalement dangereux et change la sémantique du langage. Pour moi, c'est à l'étape «Si vous allez utiliser cela, utilisez simplement une autre langue». Je réévaluerai ce choix plus tard, s'il change.

-O3nous reçoit un tas de swift_retainet swift_releaseappelle qui, honnêtement, ne semblent pas qu'ils devraient être là pour cet exemple. L'optimiseur aurait dû éluder (la plupart) AFAICT, car il connaît la plupart des informations sur le tableau et sait qu'il y a (au moins) une référence forte.

Il ne devrait pas émettre plus de rétention lorsqu'il n'appelle même pas de fonctions qui pourraient libérer les objets. Je ne pense pas qu'un constructeur de tableau puisse renvoyer un tableau qui est plus petit que ce qui a été demandé, ce qui signifie que de nombreux contrôles émis sont inutiles. Il sait également que l'entier ne sera jamais supérieur à 10k, donc les vérifications de débordement peuvent être optimisées (non pas à cause de l' -Ofastétrangeté, mais à cause de la sémantique du langage (rien d'autre ne change cette var ni ne peut y accéder, et en ajoutant jusqu'à 10k est sans danger pour le type Int).

Le compilateur peut ne pas être en mesure de décompresser le tableau ou les éléments du tableau, car ils sont transmis à sort(), qui est une fonction externe et doit obtenir les arguments qu'il attend. Cela nous Intobligera à utiliser les valeurs indirectement, ce qui ralentirait un peu. Cela pourrait changer si la sort()fonction générique (pas de manière multi-méthode) était disponible pour le compilateur et était intégrée.

Ceci est une toute nouvelle langue (publique), et elle passe par ce que je suppose être beaucoup de changements, car il y a des gens (fortement) impliqués dans la langue Swift qui demandent des commentaires et ils disent tous que la langue n'est pas terminée et le fera changement.

Code utilisé:

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")

PS: Je ne suis pas un expert d'Objective-C ni de toutes les installations de Cocoa , Objective-C ou des runtimes Swift. Je pourrais aussi supposer certaines choses que je n'ai pas écrites.

filcab
la source
Le compilateur peut ne pas être en mesure de décompresser le tableau ou les éléments du tableau, car ils sont passés à sort (), qui est une fonction externe et doit obtenir les arguments attendus. Cela ne devrait pas avoir d'importance pour un compilateur relativement bon. Passer des métadonnées (dans le pointeur - 64bits offrent beaucoup de levée) sur les données réelles et les ramifier dans la fonction appelée.
bestsss
3
Qu'est-ce qui rend -Ofast«totalement dangereux»? En supposant que vous savez comment tester votre code et exclure les débordements.
Joseph Mark
@sjeohp: Cela suppose en fait beaucoup de choses :-) Vérifier le code et exclure les débordements est difficile à faire. D'après mon expérience (je fais du travail de compilation et j'ai vérifié de grandes bases de code), et ce que j'ai entendu de gens qui font du travail de compilation sur de grandes entreprises, obtenir des débordements et d'autres comportements non définis est difficile . Même les conseils d'Apple (juste un exemple) sur la réparation de l'UB sont parfois erronés ( randomascii.wordpress.com/2014/04/17/… ). -Ofastmodifie également la sémantique des langues, mais je ne peux pas financer de documents pour cela. Comment pouvez-vous être sûr de savoir ce qu'il fait?
filcab
@bestsss: C'est possible, mais cela pourrait ne pas être utile. Il ajoute des vérifications sur chaque accès à un Int []. Cela dépend si les tableaux d'Int et quelques autres types primitifs (vous avez, au plus, 3 bits) sont beaucoup utilisés (surtout quand vous pouvez descendre à C si vous en avez besoin). Il utilise également certains bits qu'ils pourraient vouloir utiliser si, éventuellement, ils souhaitent ajouter un GC non ARC. Il ne s'adapte pas non plus aux génériques avec plus d'un argument. Puisqu'ils ont tous les types, il serait beaucoup plus facile de spécialiser tout le code qui a touché Int [] (mais pas Int? []) Pour utiliser Int intégré. Mais vous devez vous soucier de l'interopérabilité Obj-C.
filcab
@filcab, un GC non ARC (c'est-à-dire réel) serait en fait utile, mais ils ont besoin de quelque chose qui n'est pas compatible C s'ils veulent un GC non STW vraiment simultané. Je ne m'inquiéterais pas de «chaque accès à Int[]» car cela dépend du niveau auquel le compilateur peut s'aligner et il devrait être capable d'aligner les boucles serrées avec / après quelques conseils.
bestsss
53

J'ai décidé d'y jeter un œil pour le plaisir, et voici les horaires que j'obtiens:

Swift 4.0.2           :   0.83s (0.74s with `-Ounchecked`)
C++ (Apple LLVM 8.0.0):   0.74s

Rapide

// 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()

Résultats:

Swift 1.1

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

Swift 1.2

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

Swift 2.0

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

Il semble que ce soit la même performance si je compile avec -Ounchecked.

Swift 3.0

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

Il semble y avoir eu une régression des performances de Swift 2.0 à Swift 3.0, et je vois également une différence entre -Oet -Ouncheckedpour la première fois.

Swift 4.0

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 améliore à nouveau les performances, tout en maintenant un écart entre -Oet -Ounchecked. -O -whole-module-optimizationne semble pas faire de différence.

C ++

#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] << "\n";
    cout << "Elapsed time: " << duration_cast<duration<double>>(end - start).count() << "\n";

    return 0;
}

Résultats:

Apple Clang 6.0

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

Apple Clang 6.1.0

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

Apple Clang 7.0.0

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

Apple Clang 8.0.0

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

Apple Clang 9.0.0

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

Verdict

Au moment d'écrire ces lignes, le tri de Swift est rapide, mais pas encore aussi rapide que le tri de C ++ lorsqu'il est compilé avec -O, avec les compilateurs et bibliothèques ci-dessus. Avec -Ounchecked, il semble être aussi rapide que C ++ dans Swift 4.0.2 et Apple LLVM 9.0.0.

Apprenez OpenGL ES
la source
2
En réalité, vous ne devez jamais appeler vector :: reserve () avant d'insérer dix millions d'éléments.
BJovke
Peut-être! Seul le tri est actuellement chronométré.
Apprenez OpenGL ES
34

De The Swift Programming Language:

La bibliothèque standard de la fonction de tri de Swift fournit une fonction appelée tri, qui trie un tableau de valeurs d'un type connu, en fonction de la sortie d'une fermeture de tri que vous fournissez. Une fois le processus de tri terminé, la fonction de tri renvoie un nouveau tableau du même type et de la même taille que l'ancien, avec ses éléments dans l'ordre de tri correct.

La sortfonction a deux déclarations.

La déclaration par défaut qui vous permet de spécifier une clôture de comparaison:

func sort<T>(array: T[], pred: (T, T) -> Bool) -> T[]

Et une deuxième déclaration qui ne prend qu'un seul paramètre (le tableau) et est "codée en dur pour utiliser le comparateur inférieur à".

func sort<T : Comparable>(array: T[]) -> T[]

Example:
sort( _arrayToSort_ ) { $0 > $1 }

J'ai testé une version modifiée de votre code dans une aire de jeux avec la fermeture ajoutée afin que je puisse surveiller la fonction un peu plus près, et j'ai trouvé qu'avec n réglé à 1000, la fermeture était appelée environ 11000 fois.

let n = 1000
let x = Int[](count: n, repeatedValue: 0)
for i in 0..n {
    x[i] = random()
}
let y = sort(x) { $0 > $1 }

Ce n'est pas une fonction efficace, je recommanderais d'utiliser une meilleure implémentation de la fonction de tri.

ÉDITER:

J'ai jeté un œil à la page wikipedia de Quicksort et j'ai écrit une implémentation Swift pour cela. Voici le programme complet que j'ai utilisé (dans une aire de jeux)

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
}

En utilisant cela avec n = 1000, j'ai trouvé que

  1. quickSort () a été appelé environ 650 fois,
  2. environ 6000 échanges ont été effectués,
  3. et il y a environ 10 000 comparaisons

Il semble que la méthode de tri intégrée soit (ou soit proche) du tri rapide, et soit vraiment lente ...

David Skrundz
la source
17
Je me trompe peut-être complètement, mais selon en.wikipedia.org/wiki/Quicksort , le nombre moyen de comparaisons dans Quicksort est 2*n*log(n). Soit 13815 comparaisons pour trier n = 1000 éléments, donc si la fonction de comparaison est appelée environ 11000 fois cela ne semble pas si mal.
Martin R
6
Apple a également affirmé qu'un "tri d'objet complexe" (quel qu'il soit) est 3,9 fois plus rapide dans Swift qu'en Python. Il ne devrait donc pas être nécessaire de trouver une "meilleure fonction de tri". - Mais Swift est toujours en développement ...
Martin R
6
Il fait référence au logarithme naturel.
Martin R
24
log(n)pour la complexité algorithmique se réfère conventionnellement à log base-2. La raison de ne pas indiquer la base est que la loi de changement de base pour les logarithmes n'introduit qu'un multiplicateur constant, qui est rejeté aux fins de la notation O.
minuteman3
3
Concernant la discussion sur le logarithme naturel vs le logarithme de base 2: L'énoncé précis de la page Wikipedia est que le nombre moyen de comparaisons nécessaires pour n éléments est C(n) = 2n ln n ≈ 1.39n log₂ n. Pour n = 1000, cela donne C (n) = 13815, et ce n'est pas une "notation big-O".
Martin R
18

À partir de Xcode 7, vous pouvez l'activer Fast, Whole Module Optimization. Cela devrait augmenter immédiatement vos performances.

entrez la description de l'image ici

Antoine
la source
12

Les performances de Swift Array revisitées:

J'ai écrit mon propre benchmark comparant Swift à C / Objective-C. Mon benchmark calcule les nombres premiers. Il utilise le tableau des nombres premiers précédents pour rechercher des facteurs premiers dans chaque nouveau candidat, il est donc assez rapide. Cependant, il fait des TONNES de lecture de tableau et moins d'écriture dans les tableaux.

J'ai initialement fait ce benchmark contre Swift 1.2. J'ai décidé de mettre à jour le projet et de l'exécuter avec Swift 2.0.

Le projet vous permet de choisir entre l'utilisation de tableaux Swift normaux et l'utilisation de tampons de mémoire dangereux Swift à l'aide de la sémantique des tableaux.

Pour C / Objective-C, vous pouvez soit choisir d'utiliser des NSArrays, soit des C malloc'ed arrays.

Les résultats des tests semblent assez similaires avec l'optimisation de code la plus rapide et la plus petite ([-0s]) ou l'optimisation la plus rapide et agressive ([-0fast]).

Les performances de Swift 2.0 sont toujours horribles avec l'optimisation de code désactivée, tandis que les performances C / Objective-C ne sont que modérément plus lentes.

L'essentiel est que les calculs basés sur les matrices C malloc'd sont les plus rapides, avec une marge modeste

Swift avec des tampons non sécurisés prend environ 1,19X - 1,20X plus longtemps que les tableaux malloc'd C lors de l'utilisation de l'optimisation de code la plus rapide et la plus petite. la différence semble légèrement moindre avec une optimisation rapide et agressive (Swift prend plus de 1,18x à 1,16x plus long que C.

Si vous utilisez des tableaux Swift réguliers, la différence avec C est légèrement plus grande. (Swift prend environ 1,22 à 1,23 de plus.)

Les tableaux Swift réguliers sont DRAMATICALLYplus rapides qu'ils ne l'étaient dans Swift 1.2 / Xcode 6. Leurs performances sont si proches des tableaux basés sur des tampons dangereux Swift que l'utilisation de tampons de mémoire non sécurisés ne semble plus vraiment en valoir la peine, ce qui est énorme.

BTW, les performances NSArray d'Objective-C pue. Si vous allez utiliser les objets conteneurs natifs dans les deux langues, Swift est TRÈS RAPIDEMENT plus rapide.

Vous pouvez consulter mon projet sur github à SwiftPerformanceBenchmark

Il a une interface utilisateur simple qui rend la collecte de statistiques assez facile.

Il est intéressant de noter que le tri semble être légèrement plus rapide dans Swift que dans C maintenant, mais que cet algorithme de nombre premier est toujours plus rapide dans Swift.

Duncan C
la source
8

Le problème principal qui est mentionné par d'autres mais pas suffisamment évoqué est que -O3cela ne fait rien du tout dans Swift (et ne l'a jamais fait) donc lorsqu'il est compilé avec cela, il est effectivement non optimisé ( -Onone).

Les noms des options ont changé au fil du temps, de sorte que d'autres réponses ont des indicateurs obsolètes pour les options de génération. Les options actuelles correctes (Swift 2.2) sont:

-Onone // Debug - slow
-O     // Optimised
-O -whole-module-optimization //Optimised across files

L'optimisation du module entier a une compilation plus lente mais peut optimiser entre les fichiers du module, c'est-à-dire dans chaque framework et dans le code d'application réel mais pas entre eux. Vous devez l'utiliser pour tout ce qui est essentiel aux performances)

Vous pouvez également désactiver les contrôles de sécurité pour encore plus de vitesse, mais avec toutes les assertions et conditions préalables non seulement désactivées mais optimisées sur la base qu'elles sont correctes. Si vous touchez une assertion, cela signifie que vous avez un comportement indéfini. À utiliser avec une extrême prudence et uniquement si vous déterminez que l'augmentation de la vitesse vous convient (en testant). Si vous trouvez cela utile pour du code, je recommande de séparer ce code dans un cadre distinct et de désactiver uniquement les contrôles de sécurité pour ce module.

Joseph Lord
la source
Cette réponse est désormais obsolète. Depuis Swift 4.1, l'ensemble de l'optimisation du module est un booléen distinct qui peut être combiné avec d'autres paramètres et il existe désormais un -O pour optimiser la taille. Je peux mettre à jour lorsque j'ai le temps de vérifier les drapeaux d'options exacts.
Joseph Lord
7
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) 

Ceci est mon blog sur le tri rapide - Exemple Github Quick-Sort

Vous pouvez jeter un œil à l'algorithme de partitionnement de Lomuto dans Partitionnement de la liste. Écrit en Swift.

Abo3atef
la source
4

Swift 4.1 introduit un nouveau -Osizemode d'optimisation.

Dans Swift 4.1, le compilateur prend désormais en charge un nouveau mode d'optimisation qui permet des optimisations dédiées pour réduire la taille du code.

Le compilateur Swift est livré avec de puissantes optimisations. Lors de la compilation avec -O, le compilateur essaie de transformer le code afin qu'il s'exécute avec des performances maximales. Cependant, cette amélioration des performances d'exécution peut parfois s'accompagner d'un compromis de taille de code accrue. Avec le nouveau mode d'optimisation -Osize, l'utilisateur a le choix de compiler pour une taille de code minimale plutôt que pour une vitesse maximale.

Pour activer le mode d'optimisation de la taille sur la ligne de commande, utilisez -Osize au lieu de -O.

Pour en savoir plus: https://swift.org/blog/osize/

casillas
la source