Bootstrapping einer Fingerbaumstruktur

16

Nachdem ich einige Zeit mit 2-3 Fingerbäumen gearbeitet habe, war ich bei den meisten Operationen von deren Geschwindigkeit beeindruckt. Das einzige Problem, auf das ich gestoßen bin, ist der große Aufwand, der mit der anfänglichen Erstellung eines großen Fingerbaums verbunden ist. Da das Erstellen als eine Folge von Verkettungsvorgängen definiert ist, wird eine große Anzahl nicht benötigter Fingerbaumstrukturen erstellt.

Aufgrund der Komplexität von 2-3 Fingerbäumen sehe ich keine intuitive Methode, um sie zu booten, und alle meine Suchanfragen sind leer. Die Frage ist also, wie Sie einen 2-3-Finger-Baum mit minimalem Overhead booten können.

Um es deutlich zu machen: Wenn eine Sequenz bekannter Länge n gegeben ist, wird die Fingerbaumdarstellung von S erzeugtSnS mit minimalen Operationen erzeugt.

Der naive Weg, dies zu erreichen, sind aufeinanderfolgende Aufrufe der Cons-Operation (in der Literatur der ' -Operator). Dies erzeugt jedoch n verschiedene Fingerbaumstrukturen, die alle Schichten von S für [ 1 .. i ] darstellen .nS[1 ..ich]

jbondeson
quelle
1
Gibt es Antworten auf Finger Trees: Eine einfache Allzweck-Datenstruktur von Hinze und Paterson?
Dave Clarke
@ Dave Ich habe tatsächlich aus ihrem Papier implementiert, und sie befassen sich nicht mit effizienter Erstellung.
Jbondeson
Das habe ich mir gedacht.
Dave Clarke
Könnten Sie etwas genauer beschreiben, was Sie in diesem Fall unter "Build" verstehen? Ist das eine Entfaltung?
Jbapple
@jbapple - Ich habe bearbeitet, um eine explizite zu sein, sorry für die Verwirrung.
Jbondeson

Antworten:

16

GHC der Data.Sequence‚s - replicateFunktion baut eine fingertree in Zeit und Raum, aber dies zu wissen , die Elemente aktiviert ist , die von dem get-go auf der rechten Seite der Wirbelsäule des Finger Baum gehen. Diese Bibliothek wurde von den Autoren des Originalpapiers auf 2-3 Fingerbäumen geschrieben.Ö(lgn)

Wenn Sie einen Fingerbaum durch wiederholte Verkettung erstellen möchten, können Sie möglicherweise den vorübergehenden Platzbedarf beim Erstellen reduzieren, indem Sie die Darstellung der Stacheln ändern. Die Stacheln von 2-3 Fingerbäumen werden geschickt als synchronisierte, einfach verknüpfte Listen gespeichert. Wenn Sie die Stacheln stattdessen als Deques speichern, kann beim Verketten von Bäumen möglicherweise Platz gespart werden. Die Idee ist, dass die Verkettung von zwei Bäumen gleicher Höhe Platz beansprucht, indem die Stacheln der Bäume wiederverwendet werden. Wenn 2-3 Fingerbäume wie ursprünglich beschrieben verkettet werden, können die Stacheln, die sich innerhalb des neuen Baums befinden, nicht mehr unverändert verwendet werden.Ö(1)

Kaplans und Tarjans "rein funktionale Darstellungen verketteter sortierter Listen" beschreiben eine kompliziertere Fingerbaumstruktur . In diesem Artikel (in Abschnitt 4) wird auch eine Konstruktion erörtert, die der oben von mir gemachten Deque-Anregung ähnelt. Ich glaube, die Struktur, die sie beschreiben, kann zwei Bäume gleicher Höhe in O verketten ( 1 )Ö(1) Raum und Zeit . Ist das genug Platz für Sie, um Fingerbäume zu bauen?

NB: Ihre Verwendung des Wortes "Bootstrapping" bedeutet etwas anderes als Ihre obige Verwendung. Sie bedeuten, einen Teil einer Datenstruktur mit einer einfacheren Version derselben Struktur zu speichern.

Apfel
quelle
Eine sehr interessante Idee. Ich muss das untersuchen und herausfinden, wie sich die Kompromisse für die gesamte Datenstruktur auswirken würden.
Jbondeson
Ich wollte, dass diese Antwort zwei Ideen enthält: (1) Die Replikatidee (2) Schnellere Verkettung für nahezu gleich große Bäume. Ich denke, die Replikatidee kann Fingerbäume auf sehr wenig zusätzlichem Raum erstellen, wenn die Eingabe ein Array ist.
Jbapple
Ja, ich habe beides gesehen. Tut mir leid, dass ich beide nicht kommentiert habe. Ich untersuche zuerst den Replikationscode - obwohl ich mein Haskell-Wissen definitiv so weit wie möglich ausdehne. Auf den ersten Blick sieht es so aus, als könnte es die meisten Probleme lösen, die ich habe, vorausgesetzt, Sie haben schnellen wahllosen Zugriff. Das schnelle concat könnte eine etwas allgemeinere Lösung sein, wenn kein wahlfreier Zugriff besteht.
Jbondeson
10

Als ich mich auf die ausgezeichnete Antwort von jbapple bezog replicate, aber stattdessen replicateA(auf der aufgebaut replicateist) verwendete, kam ich auf Folgendes:

--Unlike fromList, one needs the length explicitly. 
myFromList :: Int -> [b] -> Seq b
myFromList l xs = flip evalState xs $ Seq.replicateA l go
    where go = do
           (y:ys) <- get
            put ys
            return y

myFromList(in einer etwas effizienteren Version) ist bereits definiert und wird intern in verwendetData.Sequence für den Bau von Finger Bäumen , die Ergebnisse der Sorten sind.

Im Allgemeinen ist die Intuition für replicateAeinfach. replicateAbasiert auf der Funktion applicativeTree . applicativeTreeNimmt ein Stück eines Baumes einer Größe mund erzeugt einen ausgewogenen Baum, der nKopien davon enthält. Die Fälle für nbis zu 8 (ein DeepFinger) sind fest codiert. Alles darüber, und es ruft sich rekursiv auf. Das "anwendbare" Element besteht einfach darin, dass es die Konstruktion des Baums mit Threading-Effekten wie im Fall des obigen Codes mit dem Zustand verschachtelt.

Die goFunktion, die repliziert wird, ist einfach eine Aktion, die den aktuellen Status abruft, ein Element von der Oberseite entfernt und den Rest ersetzt. Bei jedem Aufruf wird die als Eingabe bereitgestellte Liste weiter nach unten verschoben.

Noch einige konkrete Hinweise

main = print (length (show (Seq.fromList [1..10000000::Int])))

Bei einigen einfachen Tests ergab sich ein interessanter Kompromiss bei der Leistung. Die obige Hauptfunktion lief mit myFromList fast 1/3 niedriger als mit fromList. Auf der anderen Seite myFromListwird ein konstanter Heap von 2 MB verwendet, während der Standard fromListbis zu 926 MB verwendet. Diese 926 MB entstehen, wenn die gesamte Liste auf einmal gespeichert werden muss. In der Zwischenzeit ist die Lösung in der myFromListLage, die Struktur in einer trägen Streaming-Art und Weise zu konsumieren. Das Problem mit der Geschwindigkeit ergibt sich aus der Tatsache, dass myFromListetwa doppelt so viele Allokationen (als Folge des Aufbaus / der Zerstörung der Staatsmonade) durchgeführt werden müssen wiefromList. Wir können diese Zuordnungen beseitigen, indem wir zu einer CPS-transformierten Zustandsmonade übergehen. Dies führt jedoch dazu, dass zu einem bestimmten Zeitpunkt weitaus mehr Speicherplatz zur Verfügung steht, da der Verlust der Faulheit erfordert, die Liste ohne Streaming zu durchlaufen.

Wenn ich dagegen nicht die gesamte Sequenz mit einer Show myFromListforciere , sondern nur den Kopf oder das letzte Element extrahiere, wird sofort ein größerer Gewinn erzielt - das Extrahieren des Kopfelements erfolgt fast augenblicklich und das Extrahieren des letzten Elements beträgt 0,8s . Währenddessen fromListkostet das Extrahieren des Kopfes oder des letzten Elements mit dem Standard ~ 2,3 Sekunden.

Dies sind alles Details und sind eine Folge von Reinheit und Faulheit. In einer Situation mit Mutation und wahlfreiem Zugriff stelle ich mir die replicateLösung strikt besser vor.

Es wirft jedoch die Frage auf, ob es eine Möglichkeit gibt, eine applicativeTreesolche umzuschreiben , myFromListdie strikt effizienter ist. Ich denke, das Problem ist, dass die anwendbaren Aktionen in einer anderen Reihenfolge ausgeführt werden als der Baum, aber ich habe nicht vollständig durchgearbeitet, wie dies funktioniert oder ob es eine Möglichkeit gibt, dies zu beheben.

sclv
quelle
4
(1) Interessant. Dies scheint der richtige Weg zu sein, um diese Aufgabe zu erledigen. Ich bin überrascht zu hören, dass dies langsamer ist als fromListwenn die gesamte Sequenz erzwungen wird. (2) Vielleicht ist diese Antwort für cstheory.stackexchange.com zu code-lastig und zu sprachabhängig. Es wäre toll, wenn Sie eine sprachunabhängige Erklärung hinzufügen könnten replicateA.
Tsuyoshi Ito
9

Während Sie mit einer großen Anzahl von zwischenliegenden Fingerbaumstrukturen fertig werden, teilen sie den größten Teil ihrer Struktur miteinander. Am Ende reservieren Sie höchstens doppelt so viel Speicher wie im Idealfall, und der Rest wird mit der ersten Sammlung freigegeben. Die Asymptotik ist die gleiche, wie sie nur sein kann, da Sie am Ende einen mit n Werten gefüllten Fingerbaum benötigen.

Sie können den Fingerbaum mit Data.FingerTree.replicateund erstellen, indem Sie FingerTree.fmapWithPosIhre Werte in einem Array nachschlagen, das die Rolle Ihrer endlichen Sequenz spielt, oder indem Sie traverseWithPossie aus einer Liste oder einem anderen Container bekannter Größe entfernen.

Ö(Logn)Ö(n)Ö(Logn)

Ö(Logn)replicateAmapAccumL

TL; DR Wenn ich das tun müsste, würde ich wahrscheinlich verwenden:

rep :: (Int -> a) -> Int -> Seq a 
rep f n = mapWithIndex (const . f) $ replicate n () 

und Index in eine feste Größe Array würde ich nur liefern (arr !)für foben.

Edward KMETT
quelle