Ich habe einen Algorithmus in Swift Beta implementiert und festgestellt, dass die Leistung sehr schlecht war. Nachdem ich tiefer gegraben hatte, stellte ich fest, dass einer der Engpässe so einfach war wie das Sortieren von Arrays. Der relevante Teil ist hier:
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
In C ++ dauert ein ähnlicher Vorgang auf meinem Computer 0,06 Sekunden .
In Python dauert es 0,6 Sekunden (keine Tricks, nur y = sortiert (x) für eine Liste von ganzen Zahlen).
In Swift dauert es 6s, wenn ich es mit dem folgenden Befehl kompiliere:
xcrun swift -O3 -sdk `xcrun --show-sdk-path --sdk macosx`
Und es dauert bis zu 88 Sekunden, wenn ich es mit dem folgenden Befehl kompiliere:
xcrun swift -O0 -sdk `xcrun --show-sdk-path --sdk macosx`
Timings in Xcode mit "Release" vs. "Debug" Builds sind ähnlich.
Was ist hier falsch? Ich konnte einen gewissen Leistungsverlust im Vergleich zu C ++ verstehen, aber keine 10-fache Verlangsamung im Vergleich zu reinem Python.
Bearbeiten: Wetter bemerkte, dass das Ändern -O3
in -Ofast
diesen Code fast so schnell laufen lässt wie die C ++ - Version! Allerdings -Ofast
ändert sich die Semantik der Sprache viel - in meinen Tests, es die Kontrollen für Integer - Überläufe und Array - Indizierung Überlauf deaktiviert . Mit -Ofast
dem folgenden Swift-Code wird beispielsweise lautlos und ohne Absturz ausgeführt (und es wird etwas Müll ausgedruckt):
let n = 10000000
print(n*n*n*n*n)
let x = [Int](repeating: 10, count: n)
print(x[n])
Also -Ofast
ist es nicht das, was wir wollen; Der springende Punkt bei Swift ist, dass wir die Sicherheitsnetze installiert haben. Natürlich haben die Sicherheitsnetze einen gewissen Einfluss auf die Leistung, aber sie sollten die Programme nicht 100-mal langsamer machen. Denken Sie daran, dass Java bereits nach Array-Grenzen sucht und in typischen Fällen die Verlangsamung um einen Faktor von weniger als 2 beträgt. In Clang und GCC müssen wir -ftrapv
(signierte) Ganzzahlüberläufe überprüfen, und es ist auch nicht so langsam.
Daher die Frage: Wie können wir in Swift eine angemessene Leistung erzielen, ohne die Sicherheitsnetze zu verlieren?
Edit 2: Ich habe noch mehr Benchmarking durchgeführt, mit sehr einfachen Schleifen nach dem Vorbild von
for i in 0..<n {
x[i] = x[i] ^ 12345678
}
(Hier ist die xor-Operation nur vorhanden, damit ich die relevante Schleife im Assembler-Code leichter finden kann. Ich habe versucht, eine Operation auszuwählen, die leicht zu erkennen, aber auch "harmlos" ist, da keine Überprüfungen erforderlich sind zu ganzzahligen Überläufen.)
Auch hier gab es einen großen Unterschied in der Leistung zwischen -O3
und -Ofast
. Also habe ich mir den Assembler-Code angesehen:
Mit
-Ofast
bekomme ich so ziemlich das, was ich erwarten würde. Der relevante Teil ist eine Schleife mit 5 Anweisungen in Maschinensprache.Mit
-O3
bekomme ich etwas, das jenseits meiner wildesten Vorstellungskraft lag. Die innere Schleife umfasst 88 Zeilen Assembler-Code. Ich habe nicht versucht, alles zu verstehen, aber die verdächtigsten Teile sind 13 Aufrufe von "callq _swift_retain" und weitere 13 Aufrufe von "callq _swift_release". Das heißt, 26 Unterprogrammaufrufe in der inneren Schleife !
Bearbeiten 3: In Kommentaren fragte Ferruccio nach Benchmarks, die in dem Sinne fair sind, dass sie nicht auf eingebauten Funktionen beruhen (z. B. Sortieren). Ich denke, das folgende Programm ist ein ziemlich gutes Beispiel:
let n = 10000
var x = [Int](repeating: 1, count: n)
for i in 0..<n {
for j in 0..<n {
x[i] = x[j]
}
}
Es gibt keine Arithmetik, daher müssen wir uns keine Gedanken über ganzzahlige Überläufe machen. Das einzige, was wir tun, sind nur viele Array-Referenzen. Und die Ergebnisse sind da - Swift -O3 verliert im Vergleich zu -Ofast um einen Faktor von fast 500:
- C ++ -O3: 0,05 s
- C ++ -O0: 0,4 s
- Java: 0,2 s
- Python mit PyPy: 0,5 s
- Python: 12 s
- Schnell-Schnell: 0,05 s
- Swift -O3: 23 s
- Swift -O0: 443 s
(Wenn Sie befürchten, dass der Compiler die sinnlosen Schleifen vollständig optimiert, können Sie sie in z. B. ändern x[i] ^= x[j]
und eine print-Anweisung hinzufügen x[0]
, die ausgegeben wird. Dies ändert nichts; die Timings sind sehr ähnlich.)
Und ja, hier war die Python-Implementierung eine blöde reine Python-Implementierung mit einer Liste von Ints und verschachtelten for-Schleifen. Es sollte viel langsamer sein als nicht optimierter Swift. Bei der Swift- und Array-Indizierung scheint etwas ernsthaft kaputt zu sein.
Bearbeiten 4: Diese Probleme (sowie einige andere Leistungsprobleme) scheinen in Xcode 6 Beta 5 behoben worden zu sein.
Zum Sortieren habe ich jetzt folgende Timings:
- clang ++ -O3: 0,06 s
- schnell-schnell: 0,1 s
- swiftc -O: 0,1 s
- schnell: 4 s
Für verschachtelte Schleifen:
- clang ++ -O3: 0,06 s
- schnell-schnell: 0,3 s
- swiftc -O: 0,4 s
- schnell: 540 s
Es scheint, dass es keinen Grund mehr gibt, das unsichere -Ofast
(aka -Ounchecked
) zu verwenden; plain -O
erzeugt gleich guten Code.
xcrun --sdk macosx swift -O3
. Es ist kürzer.