Swift Beta performance: Sortieren von arrays

Ich war die Implementierung eines Algorithmus in Swift Beta und bemerkte, dass die performance war sehr schlecht. Nach dem Graben tiefer erkannte ich, dass einer der Engpässe war etwas so einfach 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++, ein ähnlicher Vorgang dauert 0.06 s auf meinem computer.

In Python, dauert es 0,6 s (keine tricks, einfach nur y = sortiert(x) für eine Liste von ganzen zahlen).

Swift dauert es 6s wenn ich es kompilieren mit folgendem Befehl:

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

Und es dauert so viel wie 88s wenn ich es kompilieren mit folgendem Befehl:

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

Timings in Xcode mit "Release" statt "Debug" baut ähnlich sind.

Was ist hier falsch? Ich könnte verstehen, dass einige performance-Verlust im Vergleich mit C++, aber nicht für eine 10-fache Verlangsamung im Vergleich zu reinem Python.


Edit: Wetter aufgefallen, dass eine änderung -O3 zu -Ofast macht dieser code laufen fast so schnell wie die C++ - version! Allerdings -Ofast ändert sich die Semantik der Sprache sehr viel — in meinen Tests, es deaktiviert die Prüfungen für integer-überläufe und array indexing overflows. Zum Beispiel, mit -Ofast den folgenden Swift-code automatisch ausgeführt wird, ohne abzustürzen (und druckt einige Müll):

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

So -Ofast ist nicht das, was wir wollen; der ganze Punkt von Swift ist, dass wir die Sicherheit der Netze statt. Natürlich, die Sicherheit der Netze haben einige Auswirkungen auf die Leistung, aber Sie sollten nicht die Programme, die 100-mal langsamer. Denken Sie daran, dass Java bereits Prüfungen für die array-Grenzen, und in typischen Fällen die Verlangsamung um einen Faktor viel weniger als 2. Und in Clang und GCC haben wir -ftrapv für die Prüfung (signed) integer-overflows, und es ist nicht so, dass langsam, entweder.

Daher die Frage: wie können wir vernünftige Leistung im Swift-ohne die Sicherheit der Netze?


Edit 2: hab ich etwas mehr benchmarking, mit sehr einfachen Schleifen entlang der Linien von

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

(Hier die xor-operation ist es einfach so, dass ich leichter finden Sie die entsprechenden Schleife in Assembler-code. Ich habe versucht zu Holen ein Vorgang, der leicht zu erkennen ist, sondern auch "harmlos" in dem Sinne, dass es sollte nicht verlangen, für alle Prüfungen im Zusammenhang zu integer-overflows.)

Wieder, es war ein großer Unterschied in der Leistung zwischen -O3 und -Ofast. Also ich hatte einen Blick auf den assembly-code:

  • Mit -Ofast ich bekomme ziemlich viel, was ich erwarten würde. Der relevante Teil ist eine Schleife mit 5 Maschinenbefehle.

  • Mit -O3 ich etwas bekommen, das war jenseits meiner wildesten Phantasie. Die innere Schleife überspannt 88 Zeilen Assembler-code. Ich habe nicht versucht, alles zu verstehen, aber die meisten verdächtigen Teile sind 13 Aufrufe von "callq _swift_retain" und weitere 13 Aufrufe von "callq _swift_release". Das ist, 26 Unterprogramm-Aufrufe in der inneren Schleife!


Edit 3: In den Kommentaren, Ferruccio gebeten, für benchmarks, die sind fair in dem Sinne, dass Sie nicht verlassen sich auf built-in Funktionen (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]
    }
}

Gibt es keine arithmetische, so brauchen wir nicht zu befürchten, integer-overflows. Das einzige, was wir tun, ist einfach nur eine Menge von array-Referenzen. Und die Ergebnisse sind hier—Swift -O3 verliert durch einen Faktor von fast 500 im Vergleich mit -Ofast:

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

(Wenn Sie besorgt sind, dass der compiler kann optimieren-aus der sinnlos-Schleifen vollständig, können Sie es ändern, um z.B. x[i] ^= x[j], und fügen Sie eine print-Anweisung, Ausgänge x[0]. Dies ändert nichts an; die Zeiten werden sich sehr ähnlich sein.)

Und ja, hier ist die Python-Implementierung war eine dumme, Reine Python-Implementierung mit einer Liste von ints und verschachtelte for-Schleifen. Es sollte viel langsamer als die nicht optimierte Swift. Irgendwas scheint ernst zu werden gebrochen, mit Swift und array-Indizierung.


Edit 4: Diese Probleme (wie auch einige andere performance-Probleme) scheint fest in der Xcode 6 beta 5.

Zum Sortieren, ich habe jetzt folgende timings:

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

Geschachtelte Schleifen:

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

Es scheint, dass es keinen Grund mehr zu verwenden, die die unsichere -Ofast (ein.k.ein. -Ounchecked); Ebene -O produziert gleichermaßen guten code.

  • Hier ist ein weiteres "Swift 100 mal langsamer als C" - Frage: stackoverflow.com/questions/24102609/...
  • Und hier ist die Diskussion über apples marketing-material im Zusammenhang mit Swift eine gute Leistung in der Sortierung: programmers.stackexchange.com/q/242816/913
  • Es wäre noch interessant/informativ, um zu sehen, einen Vergleich zu einem sort-Funktion in Python implementiert. Python ist sorted() Funktion ist Teil seiner Laufzeit, die (glaube ich) ist in C geschrieben.
  • Siehe edit 3. (Es ist nicht eine Art Funktion, aber ich denke es zeigt sehr gut, welche Art von code schlecht in Swift im Vergleich mit allem anderen, einschließlich Python.)
  • Können Sie vergleichen Sie es mit Java auch?
  • Getan. (Übrigens, eine naive Java-compiler erzeugt langsameren code als eine naive Swift-compiler. In Java berechnen x[i] müssen Sie zunächst prüfen, ob x != null und dann x.length > i. In Swift können wir überspringen Sie die erste Prüfung. Dennoch, wie wir in den benchmarks, Java gewinnt Swift -O3 um einen Faktor von ca. 100.)
  • Haben Sie gesehen, die zum Teil aus der "the Swift Programming Language" iBook über for-Schleifen? Es sagt, dass "[i] ist eine Konstante, deren Wert automatisch zu Beginn jeder iteration der Schleife.". Vielleicht erklärt es als var i: Int vor der Schleife wird die Dinge ändern?
  • Hängt von der Plattform ab. Null-check nicht erforderlich, wenn die Plattform den virtuellen Speicher und verwendet nicht die niedrigen Speicher-Adressen als gültige Speicherbereiche (z.B. Windows und ich denke, anderen Betriebssysteme auch); die MMU behandelt die null-check in diesem Fall. Überhaupt nicht überraschend, dass eine Marke neue front-end für eine neue Sprache ist schlimmer als eine 6 Jahre alte, Reife, front-end. Ich vermute, Apple wird dieses Problem beheben, bevor Swift ist aus der beta.
  • Sie können kompilieren mit: xcrun --sdk macosx swift -O3. Es ist kürzer.
  • Dieser link zeigt einige andere grundlegende Operationen im Vergleich zu Objective-C.
  • Denken Sie daran, dass Java bereits Prüfungen für die array-Grenzen, gebunden Kontrollen sind sehr wahrscheinlich zu sein entfernt, weil, wenn der compiler beweisen kann, dass. Java sollte laufen, ziemlich viel wie C (wenn richtig aufgewärmt) in diesem einfachen Fall. Null-Prüfungen sind in der Regel nicht direkt ausgeführt, sondern gefangen von der hardware und der compiler beweisen kann, x[i] ist nicht null für sicher - hat der compiler die jenseits von dumm, um tatsächlich überprüfen Sie für x null.
  • was ist falsch mit der Verwendung von swift 's" Sicherheitsnetze " in Entwicklung-und speichern -Ofast für den release?
  • Sie brauchen das "Sicherheitsnetz" bei der Produktion als input variiert. Anders ist es, um Prozess-Werte zwischen 1-10 und multiplizieren Sie Sie im Vergleich zum multiplizieren von Werten im Bereich von 2^31. Zum Beispiel die berühmt-berüchtigten heartbleed-bug wurde verursacht durch einen Mangel an range-check.
  • sicher, aber wenn Sie sich der Risiken bewusst, dann sicherlich können Sie bereinigen Ihre Eingänge, wo notwendig, um zu garantieren, dass der überlauf nicht auftreten
  • nicht sagen, es ist ideal, aber wenn die Leistung ist die Priorität, dann die Risiken zumindest scheinen überschaubar
  • um es einfach auszudrücken, wir Leben nicht in einer perfekten Welt und versucht, zu tun, was Sie vorschlagen, in 1 M LoC-Projekte ist weit härter als man sich das vorstellt. Bugs tun "exis", "stack-overflow" (name der Website) war einer der häufigsten (und immer noch ist) und bevor die no-execute-bit verwendet, um die Ausführung von beliebigem code ermöglichen sehr oft. Java läuft mit voller Bandbreite prüft, ob alle die Zeit und es kommt wirklich nicht auf die Leistung auswirken, dass die Prüfungen und nicht, anmutig, ist eine große Leistung, für die Sprache. In den letzten Jahren gab es eine riesige Sicherheitslücke, durch Umgehung es über die Unsichere scheinbar gut getan-code.
  • Jeder weiß, dass jede iteration auf iOS oder OS X, die mehr als 10000 Iterationen durchgeführt werden sollte, die in C oder C++. Wo ist die überraschung? Ist das eine rhetorische Wendung in Frage?
  • Übrigens -Ofast deaktiviert auch die Prüfungen für unwrapping nils; kompilieren Sie und führen Sie dieses "erfolgreich": let s: Double? = nil; println(s!)
  • Mit Beta 5 es wurden beträchtliche Verbesserung in Swift ' s Geschwindigkeit-siehe diesem post von Jesse Squires für weitere Details.
  • Wird Sie auch dieses update für Swift 2.0, wie es behauptet, die weitere Leistung zu erhöhen. In meinen eigenen tests fand ich heraus, dass, es sei denn, Sie kompilieren mit -Ounchecked es ist 100000 langsamer, selbst für einfache loop-tests. Mit -Ounchecked es ist "nur" 50 mal langsamer. Immer noch weht es von Python aus dem Wasser, in beiden Fällen.
  • Die java-Zahl scheint hoch, so dass ich es selber getestet und bekam mal von 50-60ms, die zum ausführen der code für "=" und 60-80ms, wenn ich die "^=". Hast du den VM-Start Zeit in diesen zahlen, oder vielleicht haben Sie gemeint .02s? Java ist in der Regel so schnell wie C für diese Art von operation. Auch java läßt sich etwa .04(=) und .06(^=) wenn ich die Schleife wiederholt (so dass Java-Zeit, um es zu kompilieren in optimierter Maschinensprache). Die .04 können beinhalten test-breaking-Optimierungen obwohl.

Schreibe einen Kommentar