Swift Bêta de la performance: tableaux de tri

J'ai été la mise en œuvre d'un algorithme rapide de la Bêta et a remarqué que la performance était très pauvre. Après pour aller plus loin, j'ai réalisé que l'un des goulots d'étranglement a été quelque chose d'aussi simple que le tri 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, il faut 0,6 s (pas de trucs, juste y = sorted(x) pour une liste d'entiers).

Dans Swift prend 6s si je compile avec la commande suivante:

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

Et il faut autant que 88s si je compile avec la commande suivante:

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

Timings dans Xcode avec "Libération" et "Debug" versions sont similaires.

Quel est le problème ici? J'ai pu comprendre certaines pertes de performances en comparaison avec le C++, mais pas 10 fois de ralentissement en comparaison avec pur Python.


Edit: météo remarqué que le changement de -O3 à -Ofast fait ce code exécuté presque aussi rapide que la version C++! Cependant, -Ofast la sémantique de la langue beaucoup dans mes tests, il désactivé le vérifie les débordements d'entiers et le tableau d'indexation des débordements. Par exemple, avec -Ofast suivantes Swift code s'exécute en mode silencieux sans s'écraser (et imprime des ordures):

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

Donc -Ofast n'est pas ce que nous voulons; le point de l'ensemble de Swift est que nous avons les filets de sécurité en place. Bien sûr, les filets de sécurité qui ont une incidence sur les performances, mais ils ne devraient pas faire les programmes 100 fois plus lent. Rappelez-vous que Java est déjà vérifie les limites du tableau, et dans les cas typiques, le ralentissement de l'activité par un facteur inférieur à 2. Et dans le Bruit et la GCC, nous avons obtenu -ftrapv pour le contrôle (signé) des débordements d'entiers, et il n'est pas lent, soit.

D'où la question: comment pouvons-nous obtenir des performances acceptables en Swift sans perdre les filets de sécurité?


Edit 2: j'ai fait un peu plus d'analyse comparative, très simples boucles le long de la lignes de

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

(Ici l'opération xor est là juste pour que je puisse plus facilement s'y retrouver boucle dans le code assembleur. J'ai essayé de chercher une opération qui est facile à repérer, mais aussi "inoffensif" dans le sens où il ne devrait pas exiger des vérifications liées à des débordements d'entiers.)

Encore une fois, il y avait une énorme différence dans les performances entre les -O3 et -Ofast. J'ai donc eu un coup d'oeil à l'assemblée de code:

  • Avec -Ofast je obtenir à peu près ce que je m'attends. La partie pertinente est une boucle avec 5 instructions en langage machine.

  • Avec -O3 je reçois quelque chose qui était au-delà de mon imagination la plus folle. La boucle interne s'étend sur 88 lignes de code assembleur. Je n'ai pas essayer de tout comprendre, mais la plupart des suspects pièces sont 13 invocations de "callq _swift_retain" et l'autre de 13 invocations de "callq _swift_release". C'est, 26 sous-routine appels dans la boucle interne!


Edit 3: Dans les commentaires, Ferruccio demandé pour les points de référence qui sont juste dans le sens où ils ne reposent pas sur des fonctions intégrées (par exemple, 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'est pas de l'arithmétique, de sorte que nous n'avons pas besoin de s'inquiéter au sujet des débordements d'entiers. La seule chose que nous faisons est juste beaucoup de références de tableau. Et les résultats sont ici—Swift -O3 perd par un facteur de près de 500 en comparaison avec d'-Ofast:

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

(Si vous craignez que le compilateur peut optimiser l'inutile boucles entièrement, vous pouvez la changer par exemple x[i] ^= x[j], et ajouter une instruction d'impression que les sorties x[0]. Cela ne change rien; les horaires sont très similaires.)

Et oui, ici, le Python de la mise en œuvre a été un stupide pur Python de mise en œuvre avec une liste d'entiers et des boucles for imbriquées. Il devrait être beaucoup plus lent que unoptimized Swift. Quelque chose semble être brisé avec Swift et tableau d'indexation.


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

Pour le tri, j'ai maintenant le minutage suivant:

  • 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 a aucune raison de plus pour utiliser le dangereux -Ofast (un.k.un. -Ounchecked); plaine -O produit d'aussi bons code.

  • Voici un autre "Swift 100 fois plus lent que C" question: stackoverflow.com/questions/24102609/...
  • Et voici la discussion sur l'Apple matériel de marketing liées à la Swift est une bonne performance dans le tri: programmers.stackexchange.com/q/242816/913
  • Il serait plus intéressant et instructif de voir une comparaison à une fonction de tri mis en œuvre en Python. Python sorted() fonction est une partie de son exécution, ce qui (je crois) est écrit en C.
  • Voir modifier 3. (Ce n'est pas une fonction de tri, mais je pense que cela montre très bien ce genre de code fonctionne mal dans Swift en comparaison avec tout le reste, y compris Python.)
  • Pouvez-vous le comparer Java trop?
  • Fait. (En passant, un naïf compilateur Java devrait produire plus lent que le code naïf Swift compilateur. En Java pour calculer x[i] vous devez d'abord vérifier que x != null et alors que x.length > i. En Swift, nous pouvons ignorer la première case. Néanmoins, comme nous le voyons dans les indices de référence, Java gagne Swift -O3 par un facteur env. 100.)
  • Avez-vous vu la pièce de la "La Swift en Langage de Programmation" iBook sur les boucles for? Il est dit que "[i] est une constante dont la valeur est automatiquement définie au début de chaque itération de la boucle.". Peut-être déclarer comme var i: Int avant la boucle va changer les choses?
  • Dépend de la plate-forme. Null check pas nécessaire si la plate-forme virtuelle de la mémoire et de ne pas utiliser le peu de mémoire que les adresses valides emplacements de mémoire (par exemple, Windows et je pense que d'autres Systèmes d'exploitation de trop); la MMU les poignées de la valeur null est à vérifier dans ce cas. Pas surprenant du tout que une nouvelle marque de front-end pour une nouvelle langue n'est pire qu'un vieux de 6 ans, mature, front-end. Je soupçonne Apple va corriger cela avant de Swift est sortie de la bêta.
  • Vous pouvez compiler avec: xcrun --sdk macosx swift -O3. C'est plus court.
  • Ceci lien montre quelques autres opérations de base de comparaison Objective-C.
  • Rappelez-vous que Java est déjà vérifie les limites du tableau, lié contrôles sont très susceptibles d'être retirés que lorsque le compilateur peut prouver que. Java devrait fonctionner à peu près comme C (une fois bien réchauffé) dans ce cas simple. Null contrôles ne sont généralement pas effectuées directement, mais pris au piège par le matériel et le compilateur peut prouver x[i] n'est pas nul, bien sûr, le compilateur doit être au-delà de stupide pour vérifier pour x null.
  • quel est le problème avec l'aide de swift "filets de sécurité" dans le développement et l'épargne-Ofast pour la libération?
  • vous avez besoin de la "filet de sécurité" au cours de la production que l'entrée varie. C'est différent pour traiter les valeurs entre 1 et 10 et de les multiplier par rapport à la multiplication des valeurs de l'ordre de 2^31. Par exemple, l'infâme heartbleed bug a été causé par un manque de contrôle de portée.
  • bien sûr, mais si vous êtes conscient des risques alors sûrement, vous pouvez désinfecter vos entrées si nécessaire, afin de garantir que le trop-plein ne se produira pas
  • pas dire que c'est l'idéal, mais si la performance est la priorité, puis les risques au moins semblent gérables
  • pour le dire simplement, nous ne vivons pas dans un monde parfait, et en essayant de faire ce que vous suggérez dans 1M LoC projets est beaucoup plus difficile alors que vous ne l'imaginez. Les Bugs ne le exis, de débordement de pile (nom du site) a été l'un des plus répandus (et est toujours) et devant le non-exécution de bits utilisés pour permettre l'exécution de code arbitraire très souvent. Java fonctionne avec la gamme complète des vérifications de tous les temps et il n'a pas vraiment d'incidence sur la performance, ayant les contrôles et à défaut gracieusement est un grand exploit pour la langue. Au cours des dernières années, il y a une énorme faille de sécurité en raison de contournement via Dangereux en apparence bien fait le code.
  • Tout le monde sait que tout itération sur iOS ou OS X qui a plus de 10000 itérations doit être fait en C ou C++. Où est la surprise? Est-ce un rethorical question?
  • Par la voie, -Ofast désactive également des contrôles pour déballer nils; vous pouvez compiler et exécuter ce "succès": let s: Double? = nil; println(s!)
  • Avec la version Beta 5 il y a eu une amélioration substantielle de Swift vitesse -- voir ce post par Jesse Squires pour plus de détails.
  • Serez-vous également de mettre à jour cette Swift 2.0 comme il le prétend augmentation des performances. Dans mes tests, j'ai découvert que si vous compilez avec -Ounchecked c'est 100 000 plus lent, même pour une simple boucle de tests. Avec -Ounchecked il est "seulement" 50 fois plus lent. Encore, il souffle en Python hors de l'eau dans les deux cas.
  • La java chiffre semble élevé, de sorte que j'ai testé moi-même et a obtenu de temps de 50-60 ms pour exécuter le code pour le "=" et 60-80 ms si j'utilise le "^=". Avez-vous d'inclure le temps de démarrage de la VM dans ces chiffres, ou peut-être vous avez voulu dire .02? Java est généralement aussi vite que C pour ce type d'opération. Aussi java installe sur .04(=) et .06(^=) quand je lance la boucle (permettant de Java le temps de les compiler en une machine optimisée de la langue). Le .04 peut inclure d'essai-rupture des optimisations bien.