2015-05-15 12 views
10

Ich lerne derzeit Haskell und ich bin gespannt zu folgenden Themen:Liste Manipulation Leistung in Haskell

Wenn ich ein Element zu einer Liste in Haskell hinzufügen, Haskell gibt eine (? Komplett) neue Liste, und nicht manipuliere das Original.

Jetzt sagen wir, ich habe eine Liste von einer Million Elemente und ich füge ein Element am Ende an. Kopiert Haskell die gesamte Liste (1 Million Elemente) und fügt das Element zu dieser Kopie hinzu? Oder gibt es einen netten "Trick" hinter den Kulissen, um das Kopieren der ganzen Liste zu vermeiden?

Und wenn es keinen "Trick" gibt, ist der Prozess des Kopierens großer Listen nicht so teuer, wie ich denke, es ist?

Antwort

8

Es hängt von der Datenstruktur ab, die Sie verwenden. Wenn Sie normale Haskell-Listen verwenden, sind diese analog zu einer typischen verketteten Listenimplementierung in C oder C++. Bei dieser Struktur sind Anhängen O (n) -Komplexität, während Vorkommata O (1) -Komplexität sind. Wenn Sie versuchen, eine Million Elemente anzufügen, dauert es O (500000500000) Zeit (O (1) + O (2) + O (3) + ... + O (1000000)) ungefähr 500000500000 Operationen. Dies ist unabhängig davon, welche Sprache Sie verwenden, Haskell, C, C++, Python, Java, C# oder sogar Assembler.

Wenn Sie jedoch eine Struktur wie Data.Sequence.Seq verwenden, verwendet es intern die richtige Struktur, um O (1) vorzuspenden und anzuhängen, aber die Kosten sind, dass es ein bisschen mehr RAM aufnehmen kann. Alle Datenstrukturen haben Kompromisse, aber es liegt an Ihnen, welche Sie verwenden möchten. Alternativ können Sie auch Data.Vector.Vector oder Data.Array.Array verwenden, die beide zusammenhängende Speicherfelder mit fester Länge bereitstellen, das Anhängen und Voranlegen ist jedoch teuer, da Sie das gesamte Array an einen neuen Speicherort im RAM kopieren müssen. Das Indizieren ist jedoch O (1), und das Mappen oder Falten einer dieser Strukturen wäre viel schneller, da Teile des Arrays gleichzeitig in Ihren CPU-Cache passen können, im Gegensatz zu verketteten Listen oder Sequenzen, bei denen Elemente überall verstreut sind dein RAM.

"Kopiert" Haskell die gesamte Liste (1 Million Elemente) und fügt das Element zu dieser Kopie hinzu?

Nicht unbedingt, kann der Compiler bestimmen, ob es sicher ist, hat gerade den letzten Punkt next Zeiger Änderung des Wertes auf dem neuen Wert anstelle der leeren Liste, oder wenn es nicht sicher ist, kann es notwendig sein, um die gesamte Liste zu kopieren . Diese Probleme sind jedoch in der Datenstruktur und nicht in der Sprache zu finden. Im Allgemeinen würde ich sagen, dass Haskells Listen besser sind als C-verknüpfte Listen, weil der Compiler eher in der Lage ist, zu analysieren, wann dies sicher ist als ein Programmierer, und C-Compiler wird diese Art von Analyse nicht machen, sie tun genau so wird erzählt.

+1

Ich bin damit einverstanden, was Sie sagen, aber die Ihre Big O-Notation ist nicht korrekt. O (500000500000) == O (1) == konstante Zeit (siehe http://en.wikipedia.org/wiki/Big_O_notation#Multiplication_by_a_constant). Sicher, Sie können argumentieren, dass, wenn Sie versuchen, "eine Million Elemente anzuhängen", es immer in O (1) läuft, da keine Variable übrig ist und die Operation "eine Million Mal anhängen" tatsächlich in konstanter Zeit läuft. Aber ich glaube nicht, dass du das sagen willst. –

+0

@ JohannesWeiß Besser? – bheklilr

+0

Ja, @bheklilr, danke :) –

3

Bei Listen ist das Anhängen teuer und die Liste muss kopiert werden, nicht aber die Elemente. Das Voranstellen ist auch billig, da der neue Wert nur auf die ursprüngliche Liste verweist.

Anfügen "third" an ["first", "second"]: Die neue Liste ist (:) "first" ((:) "second" ((:) "third" [])). Daher muss der erste Konstruktor ein neuer Konstruktor sein, da das zweite Argument ein neuer Wert sein muss wie ... Die Strings sind jedoch nicht dupliziert. Die neue Liste verweist auf dieselben Zeichenfolgen im Speicher.

Beachten Sie, dass in dem Fall, in dem der alte Wert verworfen wird, der Compiler entscheiden könnte, ihn wiederzuverwenden, anstatt Speicher für neue Werte zuzuordnen und die alten zu sammeln. In jedem Fall wird das Anhängen in O (n) durchgeführt, da es das Ende davon finden muss.

Nun, wenn Ihr Programm eine große Anzahl an Listen anhängt, möchten Sie möglicherweise andere Datenstrukturen verwenden, um O (1) wie DList aus dem Paket dlist anhängen zu können. (https://hackage.haskell.org/package/dlist-0.5/docs/Data-DList.html)

+0

die Anhänge sind nicht das Problem. Nichts schließt Listen aus, die mit ihren Elementen, die in einem großen vorher zugewiesenen Array gespeichert sind, plus "Start" - und "End" -Position implementiert werden. Sowohl 'xs' als auch' xs ++ [a] 'können dasselbe Array verwenden. Selbst Vorstufen sind kein Problem, wenn wir in der Mitte beginnen oder Listen (/ Arrays) von (Pointern auf) Array-Blöcken verwenden. es sind die * Einfügungen *, die problematisch sind. 'case xs von (a: as) ...' würde einfach 'as = (start + 1, end, array)' von 'xs = (start, end, array)' hinter den Kulissen erzeugen. –

8

Dies ist eine überraschend komplexe Frage, weil zwei Merkmale von Haskell und GHC:

  1. Verzögerte Auswertung
  2. Liste Fusion

Liste Fusion bedeutet, dass in einigen Situationen GHC kann den Listenverarbeitungscode in eine Schleife schreiben, die keine Listenzellen zuweist. Je nach dem Kontext, in dem es verwendet wird, kann derselbe Code keine zusätzlichen Kosten verursachen.

Faule Bewertung bedeutet, dass wenn das Ergebnis einer Operation nicht verbraucht wird, Sie die Kosten für die Berechnung nicht übernehmen. So zum Beispiel, ist dies günstig, weil Sie nur die ersten zehn Elemente der Liste zu konstruieren haben:

example = take 10 ([1..1000000] ++ [1000001]) 

In der Tat, in diesem Code die take 10 mit der Liste Anfügen verschmelzen kann, so ist es das gleiche wie einfach [1..10].

Aber nehmen wir einfach an, dass wir alle Elemente von allen Listen verbrauchen, die wir machen, und dass der Compiler unsere Listenoperationen nicht verschmilzt. Jetzt zu Ihren Fragen:

Wenn ich ein Element zu einer Liste in Haskell hinzufüge, gibt Haskell eine (vollständig?) Neue Liste zurück und manipuliert die ursprüngliche nicht. Nun sagen wir, ich habe eine Liste von Millionen Elementen und füge ein Element am Ende an. Kopiert Haskell die gesamte Liste (1 Million Elemente) und fügt das Element zu dieser Kopie hinzu? Oder gibt es einen netten "Trick" hinter den Kulissen, um das Kopieren der ganzen Liste zu vermeiden?

Es gibt Tricks, das Kopieren der gesamten Liste zu vermeiden, aber von seinem Ende anhängt Sie sie besiegen. Die Sache zu verstehen ist, dass funktionale Datenstrukturen normalerweise so entworfen sind, dass Operationen, die sie "modifizieren", Struktur-Teilen ausnutzen, um so viel von der alten Struktur wie möglich wiederzuverwenden. So kann beispielsweise zwei Listen anhängen wie folgt definiert werden:

(++) :: [a] -> [a] -> [a] 
[] ++ ys = ys 
(x:xs) ++ ys = x : xs ++ ys 

bei dieser Definition suchen, können Sie sagen, dass die Liste ys wird im Ergebnis wiederverwendet werden. Also, wenn wir xs = [1..3] haben, ys = [4..5] und xs ++ ys, die alle komplett ausgewertet und im Speicher auf einmal beibehalten, wird es so etwas wie dieses Gedächtnis weise aussehen:

  +---+---+ +---+---+ +---+---+ 
     xs = | 1 | -----> | 2 | -----> | 3 | -----> [] 
      +---+---+ +---+---+ +---+---+ 

      +---+---+ +---+---+ 
     ys = | 4 | -----> | 5 | -----> [] 
      +---+---+ +---+---+  
      ^
      | 
      +------------------------------------+ 
                | 
      +---+---+ +---+---+ +---+---+ | 
xs ++ ys = | 1 | -----> | 2 | -----> | 3 | -------+ 
      +---+---+ +---+---+ +---+---+ 

, dass der lange Weg, dies zu sagen: wenn Sie das tun xs ++ ys , und es fusioniert nicht, und Sie verbrauchen die ganze Liste, dann wird das eine Kopie von xs erstellen, aber den Speicher für ys wiederverwenden.

Aber jetzt schauen wir uns wieder in diesem Bit Ihrer Frage:

die wir nun sagen, ich habe eine Liste von einer Million Elementen und ich anfügen ein Element am Ende. Kopiert Haskell die gesamte Liste (1 Million Elemente) und fügt das Element zu dieser Kopie hinzu?

Das wäre so etwas wie [1..1000000] ++ [1000001], und ja, es würde die ganze Million Elemente kopieren. Auf der anderen Seite würde [0] ++ [1..1000000] nur die [0] kopieren. Die Faustregel lautet wie folgt:

  • Hinzufügen von Elementen am Anfang einer Liste ist am effizientesten.
  • Das Hinzufügen von Elementen am Ende einer Liste ist oft ineffizient, besonders wenn Sie es immer und immer wieder tun.

Die allgemeinen Lösungen für diese Art von Problem sind:

  1. Ihren Algorithmus ändern, so dass Sie eine Liste in einem Zugriffsmuster sie effizient unterstützen verwenden.
  2. Verwenden Sie keine Listen; Verwenden Sie eine andere Sequenzdatenstruktur, die das für das jeweilige Problem erforderliche Zugriffsmuster effizient unterstützt. Eine andere Antwort erwähnt Differenzlisten, aber andere erwähnenswert sind:
+0

Schön! Ich wusste nichts von Struktur-Sharing. – Robin