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 -O3
de -Ofast
ce code rend le code presque aussi rapide que la version C ++! Cependant, cela -Ofast
change 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 -Ofast
le 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 -Ofast
n'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 -ftrapv
pour 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 -O3
et -Ofast
. J'ai donc jeté un œil au code assembleur:
Avec,
-Ofast
j'obtiens à peu près ce à quoi je m'attendais. La partie pertinente est une boucle avec 5 instructions en langage machine.Avec,
-O3
j'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 -O
produit un code tout aussi bon.
xcrun --sdk macosx swift -O3
. C'est plus court.