Schnelles Zusammenführen der Funktionen

8

Hier ist meine Implementierung von merge sort in Scala:

%Vor%

Es funktioniert korrekt und es scheint asymptotisch ist es auch gut, aber es ist viel langsamer (ca. 10 mal) als Java-Implementierung von hier Ссылка und verwendet viel Speicher. Gibt es eine schnellere Implementierung von merge sort, die funktional ist? Natürlich ist es möglich, Java-Version Zeile für Zeile zu portieren, aber das ist nicht das, wonach ich suche.

UPD: Ich habe Stream in List und #:: in :: geändert und die Sortierroutine wurde schneller, nur drei bis vier Mal langsamer als die Java-Version. Aber ich verstehe nicht warum stürzt es nicht mit Stack Overflow ab? merge ist nicht tail-rekursiv, alle Argumente werden streng ausgewertet ... wie ist das möglich?

    
synapse 22.09.2013, 13:47
quelle

3 Antworten

3

Sie haben mehrere Fragen gestellt. Ich versuche, sie in einer logischen Reihenfolge zu beantworten:

Kein Stapelüberlauf in der Stream-Version

Du hast das nicht wirklich gefragt, aber es führt zu interessanten Beobachtungen.

In der Stream-Version verwenden Sie #:: merge(...) in der Funktion merge . Normalerweise wäre dies ein rekursiver Aufruf und könnte zu einem Stapelüberlauf für genügend große Eingabedaten führen. Aber nicht in diesem Fall. Der Operator #::(a,b) wird in class ConsWrapper[A] implementiert (es gibt eine implizite Konvertierung) und ist ein Synonym für cons.apply[A](hd: A, tl: ⇒ Stream[A]): Cons[A] . Wie Sie sehen können, ist das zweite Argument Anruf nach Name, was bedeutet, dass es lazy ausgewertet wird.

Das bedeutet, dass merge ein neu erstelltes Objekt vom Typ cons zurückgibt, das schließlich wieder zusammenführen wird. Mit anderen Worten: Die Rekursion passiert nicht auf dem Stack, sondern auf dem Heap. Und normalerweise haben Sie viel Heap.

Die Verwendung des Heaps für die Rekursion ist eine gute Methode, um sehr tiefe Rekursionen zu behandeln. Aber es ist viel langsamer als der Stapel. Also hast du die Geschwindigkeit für die Rekursionstiefe getauscht. Dies ist der Hauptgrund, warum Stream so langsam ist.

Der zweite Grund ist, dass Scala, um die Länge von Stream zu erhalten, das gesamte Stream materialisieren muss. Aber beim Sortieren der Stream müsste es sowieso jedes Element materialisieren, also tut das nicht sehr weh.

Kein Stapelüberlauf in der Listenversion

Wenn Sie Stream for List ändern, verwenden Sie tatsächlich den Stapel für die Rekursion. Jetzt könnte ein Stack Overflow passieren. Bei der Sortierung haben Sie jedoch normalerweise eine Rekursionstiefe von log(size) , normalerweise den Logarithmus der Basis 2 . Um also 4 Milliarden Eingabefelder zu sortieren, benötigen Sie etwa 32 Stack-Frames. Bei einer Standard-Stack-Größe von mindestens 320k (unter Windows haben andere Systeme größere Standardwerte), bleibt damit Platz für viele Rekursionen und damit für eine Menge zu sortierender Eingabedaten.

Schnellere funktionale Implementierung

Es kommt darauf an: -)

Sie sollten den Stapel und nicht den Heap für die Rekursion verwenden. Und Sie sollten Ihre Strategie abhängig von den Eingabedaten entscheiden:

  1. Bei kleinen Datenblöcken sortieren Sie sie mit einem einfachen Algorithmus. Die algorithmische Komplexität wird Sie nicht beißen und Sie können eine Menge Leistung erzielen, wenn Sie alle Daten im Cache haben. Natürlich könnten Sie auch Netze sortieren für die angegebenen Größen.
  2. Wenn Sie numerische Eingabedaten haben, können Sie radix sort verwenden und mit den Vektoreinheiten auf Ihrem Prozessor arbeiten oder Ihre GPU (komplexere Algorithmen finden Sie in GPU Gems ).
  3. Für mittelgroße Datenblöcke können Sie eine Divide-and-Conquer-Strategie verwenden, um die Daten in mehrere Threads aufzuteilen (nur wenn Sie mehrere Kerne haben!)
  4. Für riesige Datenblöcke verwenden Sie merge sort und teilen Sie es in Blöcke auf, die in den Speicher passen. Wenn Sie möchten, können Sie diese Blöcke im Netzwerk verteilen und im Speicher sortieren.

Verwende keinen Swap und benutze deine Caches. Verwenden Sie veränderbare Datenstrukturen, wenn Sie können und an Ort und Stelle sortieren. Ich denke, dass funktionelles und schnelles Sortieren nicht sehr gut zusammen funktioniert. Um die Sortierung wirklich schnell zu machen, müssen Sie Stateful-Operationen (z. B. In-Place-Mergesort auf veränderbaren Arrays) verwenden.

Normalerweise probiere ich das bei allen meinen Programmen aus: Benutze möglichst reinen funktionalen Stil, benutze aber stateful operations für kleine Teile, wenn machbar (zB weil es bessere Performance hat oder der Code nur mit vielen Zuständen umgehen muss und viel wird besser lesbar, wenn ich var s anstelle von val s) verwende.

    
stefan.schwetschke 26.09.2013, 07:49
quelle
2

Hier sind einige Dinge zu beachten.

Zuerst berücksichtigen Sie nicht, dass der ursprüngliche Stream so sortiert wird, dass er leer ist. Sie können dies beheben, indem Sie die anfängliche Überprüfung in sort ändern, um if(xs.length <= 1) xs zu lesen.

Zweitens können Streams unkalkulierbare Längen haben (zB Strem.from(1) ), was ein Problem darstellt, wenn Sie versuchen, die Hälfte dieser (möglicherweise unendlichen) Länge zu berechnen - Sie könnten eine Überprüfung dafür mit hasDefiniteSize oder in Erwägung ziehen ähnlich (obwohl naiv verwendet, könnte dies einige ansonsten berechenbare Ströme herausfiltern).

Schließlich kann die Tatsache, dass dies definiert ist, um mit Streams zu arbeiten, sein, was es verlangsamt. Ich habe versucht, eine große Anzahl von Läufen Ihrer Stream-Version von mergesort im Vergleich zu einer Version zu schreiben, die in Prozesslisten geschrieben wurde, und die Listenversion kam ungefähr dreimal schneller heraus (zugegebenermaßen nur für ein einziges Paar von Läufen). Dies deutet darauf hin, dass Streams auf diese Weise weniger effizient arbeiten als Listen oder andere Sequenztypen (Vector ist möglicherweise noch schneller oder verwendet Arrays gemäß der referenzierten Java-Lösung).

Das heißt, ich bin kein großer Experte für Timings und Effizienz, so dass jemand anders in der Lage sein kann, eine sachkundigere Antwort zu geben.

    
Shadowlands 22.09.2013 14:28
quelle
0

Ihre Implementierung ist eine Top-Down-Merge-Sortierung. Ich finde, dass eine Bottom-Up-Merge-Sortierung schneller ist und mit List.sorted vergleichbar ist (für meine Testfälle zufällig ausgewählte Listen von Zufallszahlen).

%Vor%     
Nicolas Rinaudo 26.09.2013 11:13
quelle

Tags und Links