Beeinträchtigt Unveränderlichkeit die Leistung in JavaScript?


88

In jüngster Zeit scheint es in JavaScript einen Trend zu geben, Datenstrukturen als unveränderlich zu behandeln. Wenn Sie beispielsweise eine einzelne Eigenschaft eines Objekts ändern müssen, erstellen Sie am besten einfach ein ganz neues Objekt mit der neuen Eigenschaft und kopieren Sie einfach alle anderen Eigenschaften aus dem alten Objekt und lassen Sie das alte Objekt müllsammeln. (So ​​verstehe ich sowieso.)

Meine erste Reaktion ist, dass es für die Leistung schlecht klingt.

Aber Bibliotheken wie Immutable.js und Redux.js werden von schlaueren Leuten als ich geschrieben und scheinen ein starkes Interesse an der Leistung zu haben. Daher frage ich mich, ob mein Verständnis von Müll (und dessen Auswirkungen auf die Leistung) falsch ist.

Gibt es Leistungsvorteile für die Unveränderlichkeit, die ich vermisse, und überwiegen sie die Nachteile, wenn ich so viel Müll verursache?


8
Sie haben zum Teil ein starkes Interesse an der Leistung, da Unveränderlichkeit (manchmal) Leistungskosten verursacht, und sie möchten diese Leistungskosten so weit wie möglich minimieren. Unveränderlichkeit an sich hat nur Leistungsvorteile in dem Sinne, dass es einfacher ist, Multithread-Code zu schreiben.
Robert Harvey

8
Nach meiner Erfahrung ist die Leistung nur für zwei Szenarien von Belang - eines, wenn eine Aktion mehr als 30 Mal in einer Sekunde ausgeführt wird, und zwei -, wenn die Auswirkungen bei jeder Ausführung zunehmen (Windows XP hat einmal einen Fehler gefunden, bei dem die Windows-Aktualisierungszeit gedauert hat) O(pow(n, 2))für jedes Update in seinem Verlauf .) Der meiste andere Code ist eine sofortige Reaktion auf ein Ereignis; Bei einem Klick, einer API-Anfrage oder ähnlichem und solange die Ausführungszeit konstant ist, spielt die Bereinigung einer beliebigen Anzahl von Objekten kaum eine Rolle.
Katana314

4
Berücksichtigen Sie außerdem, dass es effiziente Implementierungen von unveränderlichen Datenstrukturen gibt. Vielleicht sind diese nicht so effizient wie veränderbare, aber wahrscheinlich immer noch effizienter als eine naive Implementierung. Siehe z. B. Rein funktionale Datenstrukturen von Chris Okasaki
Giorgio

1
@ Katana314: 30+ mal für mich wären immer noch nicht genug um sich Sorgen um die Leistung zu machen. Ich habe einen kleinen CPU-Emulator portiert, den ich in node.js geschrieben habe, und node hat die virtuelle CPU mit etwa 20 MHz ausgeführt (das sind 20 Millionen Mal pro Sekunde). Ich würde mir also nur Gedanken über die Leistung machen, wenn ich mehr als 1000 Mal pro Sekunde etwas mache (selbst dann würde ich mir keine Sorgen machen, bis ich 1000000 Operationen pro Sekunde mache, weil ich weiß, dass ich bequem mehr als 10 auf einmal machen kann). .
Slebetman

2
@RobertHarvey "Unveränderlichkeit allein hat nur Leistungsvorteile in dem Sinne, dass es einfacher ist, Multithread-Code zu schreiben." Das ist nicht ganz richtig, die Unveränderlichkeit ermöglicht ein sehr durchdringendes Teilen ohne echte Konsequenzen. Was in einer veränderlichen Umgebung sehr unsicher ist. Dies gibt Ihnen den Eindruck, als würden Sie ein O(1)Array O(log n)in einen Binärbaum aufteilen und einfügen, während Sie den alten Baum weiterhin frei verwenden können. Ein weiteres Beispiel ist, tailsdass alle Endpunkte einer Liste tails [1, 2] = [[1, 2], [2], []]nur O(n)Zeit und Raum beanspruchen, aber O(n^2)in der Elementanzahl enthalten sind
Semikolon

Antworten:


59

Wenn Sie beispielsweise eine einzelne Eigenschaft eines Objekts ändern müssen, erstellen Sie am besten einfach ein ganz neues Objekt mit der neuen Eigenschaft und kopieren Sie einfach alle anderen Eigenschaften aus dem alten Objekt und lassen Sie das alte Objekt müllsammeln.

Ohne Unveränderlichkeit müssen Sie möglicherweise ein Objekt zwischen verschiedenen Bereichen austauschen, und Sie wissen nicht im Voraus, ob und wann das Objekt geändert wird. Um unerwünschte Nebenwirkungen zu vermeiden, erstellen Sie "nur für den Fall" eine vollständige Kopie des Objekts und geben diese Kopie weiter, auch wenn sich herausstellt, dass keine Eigenschaft geändert werden muss. Das wird viel mehr Müll hinterlassen als in Ihrem Fall.

Dies zeigt Folgendes: Wenn Sie das richtige hypothetische Szenario erstellen, können Sie alles beweisen, insbesondere was die Leistung betrifft. Mein Beispiel ist jedoch nicht so hypothetisch, wie es sich anhören mag. Ich habe letzten Monat an einem Programm gearbeitet, bei dem wir genau über dieses Problem gestolpert sind, weil wir uns ursprünglich gegen eine unveränderliche Datenstruktur entschieden und gezögert haben, dies später zu überarbeiten, weil es den Aufwand nicht wert zu sein schien.

Wenn Sie sich also Fälle wie diesen aus einem älteren SO-Beitrag ansehen , wird die Antwort auf Ihre Fragen wahrscheinlich klar - es kommt darauf an . In einigen Fällen beeinträchtigt die Unveränderlichkeit die Leistung, in einigen Fällen ist möglicherweise das Gegenteil der Fall, in vielen Fällen hängt es davon ab, wie intelligent Ihre Implementierung ist, und in noch mehr Fällen ist der Unterschied vernachlässigbar.

Ein letzter Hinweis: Ein reales Problem, auf das Sie stoßen könnten, besteht darin, dass Sie sich frühzeitig für oder gegen die Unveränderlichkeit einiger grundlegender Datenstrukturen entscheiden müssen. Dann bauen Sie eine Menge Code darauf auf und einige Wochen oder Monate später werden Sie sehen, ob die Entscheidung gut oder schlecht war.

Meine persönliche Faustregel für diese Situation lautet:

  • Wenn Sie eine Datenstruktur mit nur wenigen Attributen entwerfen, die auf primitiven oder anderen unveränderlichen Typen basieren, versuchen Sie zuerst Unveränderlichkeit.
  • Wenn Sie einen Datentyp entwerfen möchten, bei dem Arrays mit großer (oder nicht definierter) Größe, wahlfreiem Zugriff und sich ändernden Inhalten beteiligt sind, verwenden Sie Mutability.

Verwenden Sie für Situationen zwischen diesen beiden Extremen Ihr Urteilsvermögen. Aber YMMV.


8
That will leave a lot more garbage than in your case.Und um die Sache noch schlimmer zu machen, wird Ihre Laufzeit möglicherweise nicht in der Lage sein, die sinnlose Vervielfältigung zu erkennen, und daher ist sie (anders als ein abgelaufenes unveränderliches Objekt, das niemand verwendet) nicht einmal für die Sammlung geeignet.
Jacob Raihle

37

Zunächst einmal ist Ihre Charakterisierung von unveränderlichen Datenstrukturen ungenau. Im Allgemeinen wird der größte Teil einer Datenstruktur nicht kopiert, sondern gemeinsam genutzt , und nur die geänderten Teile werden kopiert. Es wird eine persistente Datenstruktur genannt . Die meisten Implementierungen können die meiste Zeit auf persistente Datenstrukturen zurückgreifen. Die Leistung ist nahe genug an veränderlichen Datenstrukturen, die von funktionalen Programmierern im Allgemeinen als vernachlässigbar angesehen werden.

Zweitens finde ich, dass viele Leute eine ziemlich ungenaue Vorstellung von der typischen Lebensdauer von Objekten in typischen Imperativprogrammen haben. Möglicherweise liegt dies an der Popularität von speicherverwalteten Sprachen. Setzen Sie sich irgendwann einmal hin und sehen Sie sich an, wie viele temporäre Objekte und defensive Kopien Sie im Vergleich zu wirklich langlebigen Datenstrukturen erstellen. Ich denke, Sie werden über das Verhältnis überrascht sein.

In funktionellen Programmierkursen, in denen ich unterrichte, wurde bemerkt, wie viel Müll ein Algorithmus erzeugt, und dann zeige ich die typische imperative Version desselben Algorithmus, der genau so viel erzeugt. Nur aus irgendeinem Grund merken es die Leute nicht mehr.

Indem Sie das Teilen von Variablen fördern und das Erstellen von Variablen entmutigen, bis Sie einen gültigen Wert für sie haben, fördert Unveränderlichkeit tendenziell sauberere Codierungspraktiken und längerlebige Datenstrukturen. Dies führt je nach Algorithmus häufig zu vergleichbaren, wenn nicht sogar niedrigeren Abfallmengen.


8
"... dann zeige ich die typische imperative Version desselben Algorithmus, der genau so viel erzeugt." Diese. Außerdem können Personen, die mit diesem Stil noch nicht vertraut sind, und insbesondere, wenn sie mit dem funktionalen Stil im Allgemeinen noch nicht vertraut sind, anfänglich suboptimale funktionale Implementierungen erstellen.
Wberry

1
"vom Erstellen von Variablen abraten" Gilt das nicht nur für Sprachen, in denen das Standardverhalten "Kopieren bei Zuweisung" / "Implizite Konstruktion" lautet? In JavaScript ist eine Variable nur ein Bezeichner. es ist kein eigenständiges Objekt. Es belegt zwar irgendwo noch Speicherplatz, dies ist jedoch vernachlässigbar (insbesondere, da meines Wissens die meisten Implementierungen von JavaScript immer noch einen Stapel für Funktionsaufrufe verwenden. Wenn Sie also nicht viel Rekursion haben, werden Sie für die meisten nur denselben Stapelspeicherplatz wiederverwenden temporäre Variablen). Unveränderlichkeit hat mit diesem Aspekt nichts zu tun.
JAB

33

Später Einsteiger in dieses Q & A mit bereits guten Antworten, aber ich wollte mich als Ausländer einmischen, der es gewohnt ist, die Dinge vom untergeordneten Standpunkt der Bits und Bytes im Speicher aus zu betrachten.

Ich bin sehr begeistert von unveränderlichen Designs, selbst aus der C-Perspektive, und von der Perspektive, neue Wege zu finden, um diese scheußliche Hardware, die wir heutzutage haben, effektiv zu programmieren.

Langsamer / schneller

Auf die Frage, ob es die Dinge langsamer macht, wäre eine roboterhafte Antwort yes. Auf dieser Art von sehr technischer konzeptioneller Ebene könnte Unveränderlichkeit die Dinge nur langsamer machen. Hardware ist am besten geeignet, wenn sie den Speicher nicht sporadisch zuweist und stattdessen nur den vorhandenen Speicher ändern kann (weshalb wir Konzepte wie zeitliche Lokalität verwenden).

Eine praktische Antwort ist jedoch maybe. Die Leistung ist nach wie vor weitgehend eine Produktivitätsmetrik in einer nicht trivialen Codebasis. Wir finden es normalerweise nicht schrecklich, Codebasen zu warten, die über die Rennbedingungen stolpern, und das am effizientesten, selbst wenn wir die Fehler ignorieren. Effizienz ist oft eine Funktion von Eleganz und Einfachheit. Die Spitze der Mikrooptimierungen kann in gewisser Weise widersprüchlich sein, diese sind jedoch normalerweise den kleinsten und kritischsten Codeabschnitten vorbehalten.

Unveränderliche Bits und Bytes transformieren

Wenn wir Röntgenkonzepte wie objectsund stringsso weiter betrachten, dann sind dies aus der Sicht der unteren Ebene nur Bits und Bytes in verschiedenen Speicherformen mit unterschiedlichen Geschwindigkeits- / Größenmerkmalen (Geschwindigkeit und Größe der Speicherhardware sind typischerweise) sich gegenseitig ausschließen).

Bildbeschreibung hier eingeben

Die Speicherhierarchie des Computers mag es, wenn wir wiederholt auf denselben Speicherblock zugreifen, wie im obigen Diagramm, da dieser häufig verwendete Speicherblock in der schnellsten Form des Speichers (L1-Cache, z. B. welcher) gespeichert wird ist fast so schnell wie ein Register). Möglicherweise greifen wir wiederholt auf denselben Speicher zu (verwenden ihn mehrmals) oder greifen wiederholt auf verschiedene Abschnitte des Chunks zu (z. B .: Durchlaufen der Elemente in einem zusammenhängenden Chunk, der wiederholt auf verschiedene Abschnitte dieses Chunks zugreift).

Wir werfen in diesem Prozess einen Schraubenschlüssel, wenn das Ändern dieses Speichers dazu führt, dass ein ganz neuer Speicherblock an der Seite erstellt werden soll, wie folgt:

Bildbeschreibung hier eingeben

... in diesem Fall kann der Zugriff auf den neuen Speicherblock obligatorische Seitenfehler und Cache-Fehler erfordern, um ihn wieder in die schnellsten Speicherformen (bis hin zu einem Register) zu verschieben. Das kann ein echter Leistungskiller sein.

Es gibt jedoch Möglichkeiten, dies zu mildern, indem ein Reservepool vorab zugewiesenen Speichers verwendet wird, der bereits angesprochen wurde.

Große Aggregate

Ein weiteres konzeptionelles Problem, das sich aus einer etwas übergeordneten Sicht ergibt, besteht darin, unnötige Kopien von wirklich großen Aggregaten in großen Mengen zu erstellen.

Um ein übermäßig komplexes Diagramm zu vermeiden, stellen wir uns vor, dieser einfache Speicherblock wäre irgendwie teuer (möglicherweise UTF-32-Zeichen auf einer unglaublich begrenzten Hardware).

Bildbeschreibung hier eingeben

Wenn wir in diesem Fall "HELP" durch "KILL" ersetzen wollten und dieser Speicherblock unveränderlich war, müssten wir einen vollständigen neuen Block erstellen, um ein eindeutiges neues Objekt zu erstellen, obwohl sich nur Teile davon geändert haben :

Bildbeschreibung hier eingeben

Diese Art tiefer Kopie von allem anderen, nur um ein kleines Teil einzigartig zu machen, kann sehr teuer sein (in realen Fällen wäre dieser Speicherblock viel, viel größer, um ein Problem darzustellen).

Trotz eines solchen Aufwands ist diese Art der Konstruktion in der Regel weitaus weniger anfällig für menschliches Versagen. Jeder, der in einer funktionalen Sprache mit reinen Funktionen gearbeitet hat, kann dies wahrscheinlich zu schätzen wissen, insbesondere in Multithread-Fällen, in denen wir solchen Code ohne Rücksicht auf die Welt multithreaden können. Im Allgemeinen neigen menschliche Programmierer dazu, Zustandsänderungen auszulösen, insbesondere solche, die externe Nebenwirkungen auf Zustände außerhalb des Bereichs einer aktuellen Funktion verursachen. Selbst die Wiederherstellung nach einem externen Fehler (Ausnahme) in einem solchen Fall kann mit veränderlichen externen Zustandsänderungen in der Mischung unglaublich schwierig sein.

Eine Möglichkeit, diese redundante Kopierarbeit zu verringern, besteht darin, diese Speicherblöcke in eine Sammlung von Zeigern (oder Verweisen) auf Zeichen zu verwandeln.

Entschuldigung, mir ist nicht klar geworden, dass wir Lbeim Erstellen des Diagramms keine eindeutigen Angaben machen müssen .

Blau zeigt flache kopierte Daten an.

Bildbeschreibung hier eingeben

... leider würde es unglaublich teuer werden, einen Zeiger / eine Referenz pro Zeichen zu bezahlen. Darüber hinaus können wir den Inhalt der Zeichen über den gesamten Adressraum verteilen und dafür in Form einer Schiffsladung von Seitenfehlern und Cache-Fehlern bezahlen, was diese Lösung noch schlimmer macht, als das gesamte Objekt zu kopieren.

Auch wenn wir darauf geachtet haben, diese Zeichen zusammenhängend zuzuweisen, kann der Computer beispielsweise 8 Zeichen und 8 Zeiger auf ein Zeichen in eine Cache-Zeile laden. Wir laden den Speicher wie folgt, um den neuen String zu durchlaufen:

Bildbeschreibung hier eingeben

In diesem Fall müssen zum Durchlaufen dieser Zeichenfolge am Ende 7 verschiedene Cachezeilen mit zusammenhängendem Speicher geladen werden, wenn im Idealfall nur 3 erforderlich sind.

Daten aufteilen

Um das obige Problem zu lösen, können wir dieselbe grundlegende Strategie anwenden, jedoch auf einer gröberen Ebene von 8 Zeichen, z

Bildbeschreibung hier eingeben

Das Ergebnis erfordert, dass 4 Datenzeilen (1 für die 3 Zeiger und 3 für die Zeichen) geladen werden, um diese Zeichenfolge zu durchlaufen, die nur 1 kurz vor dem theoretischen Optimum liegt.

Das ist also überhaupt nicht schlecht. Es gibt einige Speicherverschwendung, aber Speicher ist reichlich vorhanden, und die Verwendung von mehr verlangsamt die Dinge nicht, wenn auf den zusätzlichen Speicher nur kalte Daten zugegriffen wird, auf die nicht häufig zugegriffen wird. Nur für die heißen, zusammenhängenden Daten, bei denen Speicherbedarf und -geschwindigkeit oft Hand in Hand gehen und wir mehr Speicher in eine einzelne Seite oder Cache-Zeile einbauen und vor der Räumung darauf zugreifen möchten. Diese Darstellung ist ziemlich Cache-freundlich.

Geschwindigkeit

Die Verwendung einer Darstellung wie der oben genannten kann also zu einem angemessenen Leistungsgleichgewicht führen. Die wahrscheinlich leistungskritischste Verwendung von unveränderlichen Datenstrukturen besteht darin, klobige Datenstücke zu modifizieren und sie in diesem Prozess einzigartig zu machen, während nicht modifizierte Teile flach kopiert werden. Es bedeutet auch einen Mehraufwand für Atomoperationen, die flachen kopierten Teile sicher in einem Multithread-Kontext zu referenzieren (möglicherweise mit fortlaufender Atomreferenzzählung).

Solange diese klobigen Daten jedoch grob genug dargestellt werden, verringert sich ein Großteil dieses Overheads und ist möglicherweise sogar trivialisiert, während wir dennoch die Sicherheit und Leichtigkeit haben, mehr Funktionen in reiner Form ohne externe Seite zu codieren und zu multithreaden Auswirkungen.

Neue und alte Daten aufbewahren

Unveränderlichkeit ist aus meiner Sicht (in praktischer Hinsicht) am hilfreichsten, wenn wir versucht sind, vollständige Kopien großer Datenmengen anzufertigen, um sie in einem veränderlichen Kontext einzigartig zu machen, in dem das Ziel besteht, etwas Neues hervorzubringen Etwas, das es bereits gibt und in dem wir sowohl Neues als auch Altes bewahren wollen, wenn wir nur kleine Stücke davon mit einem sorgfältigen unveränderlichen Design einzigartig machen könnten.

Beispiel: System rückgängig machen

Ein Beispiel hierfür ist ein Undo-System. Möglicherweise ändern wir einen kleinen Teil einer Datenstruktur und möchten sowohl das ursprüngliche Formular, das rückgängig gemacht werden kann, als auch das neue Formular beibehalten. Mit dieser Art von unveränderlichem Design, das nur kleine, geänderte Abschnitte der Datenstruktur eindeutig macht, können wir einfach eine Kopie der alten Daten in einem Rückgängig-Eintrag speichern, während wir nur die Speicherkosten der hinzugefügten eindeutigen Portionsdaten bezahlen. Dies bietet ein sehr effektives Gleichgewicht zwischen Produktivität (wodurch die Implementierung eines Undo-Systems zum Kinderspiel wird) und Leistung.

Hochrangige Schnittstellen

Dennoch ergibt sich mit dem obigen Fall etwas Unangenehmes. In einem lokalen Funktionskontext lassen sich veränderbare Daten häufig am einfachsten und unkompliziertesten ändern. Der einfachste Weg, ein Array zu ändern, besteht darin, es zu durchlaufen und jeweils ein Element zu ändern. Wir könnten den intellektuellen Aufwand erhöhen, wenn wir eine große Anzahl von High-Level-Algorithmen zur Auswahl hätten, um ein Array zu transformieren, und die geeignete auswählen müssten, um sicherzustellen, dass all diese klobigen, flachen Kopien erstellt werden, während die Teile, die geändert werden, unverändert bleiben einzigartig gemacht.

In solchen Fällen ist es wahrscheinlich am einfachsten, veränderbare Puffer lokal im Kontext einer Funktion zu verwenden (in der sie uns normalerweise nicht auslösen), die atomar Änderungen an der Datenstruktur festlegt, um eine neue unveränderliche Kopie zu erhalten (ich glaube, dass einige Sprachen dies aufrufen) diese "Transienten") ...

... oder wir modellieren einfach Transformationsfunktionen höherer und höherer Ebenen über den Daten, um den Prozess des Änderns eines veränderlichen Puffers und des Festschreibens für die Struktur ohne Veränderungslogik zu verbergen. Auf jeden Fall ist dies noch kein weit verbreitetes Gebiet, und wir müssen uns auf unveränderliche Entwürfe beschränken, um aussagekräftige Schnittstellen für die Transformation dieser Datenstrukturen zu finden.

Datenstrukturen

Eine andere Sache, die hier auftritt, ist, dass die Unveränderlichkeit, die in einem leistungskritischen Kontext verwendet wird, wahrscheinlich dazu führen wird, dass Datenstrukturen in Blockdaten zerfallen, bei denen die Blöcke nicht zu klein, aber auch nicht zu groß sind.

Verknüpfte Listen möchten möglicherweise einiges ändern, um dies zu berücksichtigen und sich in entrollte Listen zu verwandeln. Große, zusammenhängende Arrays können sich in ein Array von Zeigern verwandeln, die in zusammenhängende Chunks mit Modulo-Indizierung für den wahlfreien Zugriff umgewandelt werden.

Dies kann möglicherweise die Art und Weise verändern, wie wir Datenstrukturen auf interessante Weise betrachten, und gleichzeitig die Änderungsfunktionen dieser Datenstrukturen so anpassen, dass sie voluminöser wirken, um die zusätzliche Komplexität beim flachen Kopieren einiger Bits zu verbergen und andere Bits dort eindeutig zu machen.

Performance

Wie auch immer, dies ist meine kleine untergeordnete Ansicht zum Thema. Theoretisch kann Unveränderlichkeit Kosten verursachen, die von sehr großen bis zu kleinen Kosten reichen. Ein sehr theoretischer Ansatz führt jedoch nicht immer zu einer schnellen Bewerbung. Es macht sie möglicherweise skalierbar, aber für die Geschwindigkeit in der realen Welt ist es oft erforderlich, die praktischere Denkweise zu berücksichtigen.

Aus praktischer Sicht verschwimmen Eigenschaften wie Leistung, Wartbarkeit und Sicherheit vor allem bei einer sehr großen Codebasis. Während die Leistung in gewissem Sinne mit der Unveränderlichkeit abnimmt, ist es schwierig, die Vorteile für Produktivität und Sicherheit (einschließlich Thread-Sicherheit) zu diskutieren. Eine Erhöhung dieser Werte kann häufig zu einer Steigerung der praktischen Leistung führen, schon allein deshalb, weil die Entwickler mehr Zeit haben, ihren Code zu optimieren, ohne von Fehlern überschwemmt zu werden.

Deshalb denke ich , aus diesem praktischen Sinn, unveränderlichen Datenstrukturen tatsächlich könnte helfen , die Leistung in vielen Fällen, so seltsam , wie es sich anhört. Eine ideale Welt sucht möglicherweise nach einer Mischung aus diesen beiden: unveränderlichen und veränderlichen Datenstrukturen, wobei die veränderlichen Datenstrukturen in der Regel in einem sehr lokalen Bereich (z. B. lokal für eine Funktion) sehr sicher sind, während die unveränderlichen eine externe Seite vermeiden können bewirkt sofort und macht aus allen Änderungen an einer Datenstruktur eine atomare Operation, die eine neue Version ohne das Risiko von Rennbedingungen erzeugt.


11

ImmutableJS ist eigentlich recht effizient. Wenn wir ein Beispiel nehmen:

var x = {
    Foo: 1,
    Bar: { Baz: 2 }
    Qux: { AnotherVal: 3 }
}

Wenn das obige Objekt unveränderlich gemacht wird, ändern Sie den Wert der 'Baz'-Eigenschaft wie folgt:

var y = x.setIn('/Bar/Baz', 3);
y !== x; // Different object instance
y.Bar !== x.Bar // As the Baz property was changed, the Bar object is a diff instance
y.Qux === y.Qux // Qux is the same object instance

Dies schafft einige wirklich coole Leistungsverbesserungen für tiefe Objektmodelle, bei denen Sie nur Werttypen auf Objekte auf dem Pfad zum Stamm kopieren müssen. Je größer das Objektmodell und je kleiner die vorgenommenen Änderungen sind, desto besser ist die Speicher- und CPU-Leistung der unveränderlichen Datenstruktur, da sie am Ende viele Objekte gemeinsam nutzen.

Wie die anderen Antworten bereits sagten, xist die Leistung erheblich besser , wenn Sie dies dem Versuch gegenüberstellen, dieselben Garantien durch defensives Kopieren zu bieten, bevor Sie es an eine Funktion übergeben, die es manipulieren könnte.


4

In einer geraden Linie hat unveränderlicher Code den Overhead der Objekterstellung, der langsamer ist. Es gibt jedoch viele Situationen, in denen es sehr schwierig wird, veränderlichen Code effizient zu verwalten (was zu einer Menge defensiven Kopierens führt, was auch teuer ist), und es gibt viele clevere Strategien, um die Kosten für das "Kopieren" eines Objekts zu verringern , wie von anderen erwähnt.

Wenn Sie ein Objekt wie einen Leistungsindikator haben und dieses Objekt mehrmals pro Sekunde inkrementiert wird, lohnt es sich möglicherweise nicht, wenn dieser Leistungsindikator unveränderlich ist. Wenn Sie ein Objekt haben, das von vielen verschiedenen Teilen Ihrer Anwendung gelesen wird, und jeder von ihnen einen eigenen, leicht unterschiedlichen Klon des Objekts haben möchte, fällt es Ihnen viel leichter, dies auf performante Weise zu orchestrieren, indem Sie eine gute verwenden unveränderliche Objektimplementierung.


4

Hinzufügen zu dieser (bereits hervorragend beantworteten) Frage:

Die kurze Antwort lautet ja ; Dies beeinträchtigt die Leistung, da Sie immer nur Objekte erstellen, anstatt vorhandene zu mutieren, was zu einem höheren Aufwand bei der Objekterstellung führt.


Die lange Antwort ist jedoch nicht wirklich .

Aus Sicht der tatsächlichen Laufzeit erstellen Sie in JavaScript bereits eine ganze Reihe von Laufzeitobjekten - Funktionen und Objektliterale sind in JavaScript allgegenwärtig, und niemand scheint zweimal darüber nachzudenken, diese zu verwenden. Ich würde argumentieren, dass die Objekterstellung eigentlich recht billig ist, obwohl ich keine Zitate dafür habe, sodass ich sie nicht als eigenständiges Argument verwenden würde.

Für mich ist die größte Leistungssteigerung nicht die Laufzeitleistung, sondern die Entwicklerleistung . Eines der ersten Dinge, die ich bei der Arbeit an Real World ™ -Anwendungen gelernt habe, ist, dass die Wandlungsfähigkeit wirklich gefährlich und verwirrend ist. Ich habe viele Stunden damit verbracht, einen Thread (nicht den Parallelitätstyp) der Ausführung aufzuspüren, um herauszufinden, was einen obskuren Fehler verursacht, wenn sich herausstellt, dass es sich um eine Mutation von der anderen Seite der verdammten Anwendung handelt!

Mit Unveränderlichkeit macht die Dinge viel einfacher zu Grund , über. Sie können sofort erkennen, dass sich das X-Objekt während seiner Lebensdauer nicht ändern wird, und die einzige Möglichkeit, dies zu ändern, besteht darin, es zu klonen. Ich schätze dies sehr viel mehr (besonders in Teamumgebungen) als jede Mikrooptimierung, die eine Veränderung mit sich bringen könnte.

Es gibt Ausnahmen, insbesondere die oben genannten Datenstrukturen. Ich bin selten auf ein Szenario gestoßen, in dem ich eine Karte nach der Erstellung ändern wollte (obwohl ich zugegebenermaßen eher von Pseudoobjekt-Literal-Karten als von ES6-Karten spreche), genau wie bei Arrays. Wenn Sie mit größeren Datenstrukturen arbeiten, zahlt sich die Wandlungsfähigkeit möglicherweise aus. Denken Sie daran, dass jedes Objekt in JavaScript als Referenz und nicht als Wert übergeben wird.


Ein Punkt, der oben angesprochen wurde, war jedoch die GC und ihre Unfähigkeit, Doppelspurigkeiten zu erkennen. Dies ist ein berechtigtes Anliegen, aber meiner Meinung nach ist es nur ein Anliegen, wenn das Gedächtnis ein Anliegen ist, und es gibt viel einfachere Möglichkeiten, sich in eine Ecke zu kodieren - zum Beispiel Zirkelverweise in Abschlüssen.


Letztendlich würde ich es vorziehen, eine unveränderliche Codebasis mit sehr wenigen (wenn überhaupt) veränderlichen Abschnitten zu haben und etwas weniger performant zu sein als überall eine Veränderlichkeit zu haben. Sie können später jederzeit optimieren, wenn die Unveränderlichkeit aus irgendeinem Grund die Leistung beeinträchtigt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.