2015-06-01 5 views
10

Ich erzeuge eine ~ 200000-Element-Array von Objekten (mit Objekt Literal Notation innerhalb map statt new Constructor()), und ich bin Speichern einer JSON.stringify 'd-Version davon auf der Festplatte, wo es 31 MB, einschließlich Zeilenumbrüche und Ein-Platz-pro-Einzug Ebene (JSON.stringify(arr, null, 1)).JSON.parse() auf einem großen Array von Objekten verwendet viel mehr Speicher als es sollte

Dann wird in einem neuen Knoten Prozess, las ich die gesamte Datei in eine UTF-8-String und übergeben es an JSON.parse:

var fs = require('fs'); 
var arr1 = JSON.parse(fs.readFileSync('JMdict-all.json', {encoding : 'utf8'})); 

Knoten Speicherverbrauch etwa 1,05 GB ist nach Mavericks' Activity Monitor! Selbst wenn ich in ein Terminal tippe, fühle ich mich auf meiner uralten 4-GB-RAM-Maschine lahm.

Wenn aber in einem neuen Knoten Prozess, ich die Inhalt in einen String der Datei zu laden, hacken sie an Elementgrenzen auf und JSON.parse jedes Element einzeln, angeblich immer das gleiche Objekt Array:

var fs = require('fs'); 
var arr2 = fs.readFileSync('JMdict-all.json', {encoding : 'utf8'}).trim().slice(1,-3).split('\n },').map(function(s) {return JSON.parse(s+'}');}); 

Knoten verwendet nur ~ 200 MB Arbeitsspeicher und keine merkliche Systemverzögerung. Dieses Muster bleibt bei vielen Neustarts des Knotens bestehen: JSON.parse Das gesamte Array erfordert eine Menge Speicher, während das parsen elementweise viel speichereffizienter ist.

Warum ist die Speicherbelegung so groß? Ist das ein Problem mit JSON.parse verhindern effiziente Bildung versteckter Klassen in V8? Wie kann ich eine gute Speicherleistung erzielen, ohne Saiten zu schneiden und zu schneiden? Muss ich ein Streaming-JSON-Parser verwenden?

Zum leichteren Experimentieren habe ich die JSON-Datei in Frage gestellt in einer Gist, bitte zögern Sie nicht, es zu klonen.

+0

Der von einem Prozess verbrauchte Speicher bedeutet nichts. Buchstäblich können Sie nicht über Ihre Verbrauchseffizienz des Code-Speichers basierend darauf begründen. – zerkms

+0

@zerkms danke, dass du das herausgibst. Ich hätte feststellen müssen, dass sich mein System (4 GB physikalischer RAM) tatsächlich lustvoller anfühlt, sobald ich die erste Methode ausprobiere: Ich kann es sogar beim Tippen im Terminal erkennen. –

+0

Huh. Wenn ich 'node --expose-gc' starte, führe das erste Code-Snippet aus (nutze 1 GB Speicher) und führe' global.gc(); 'aus. Etwa fünfzig Mal sinkt die Speicherbelegung des Knotens langsam auf 100 MB ab. Die Implikationen - wow. –

Antwort

6

Ein paar Punkte zu beachten:

  1. Sie haben festgestellt, dass aus irgendeinem Grund, es ist viel effizienter für jedes Element Ihres Arrays einzelnen JSON.parse() Anrufe zu tun, statt einer großen JSON.parse().
  2. Das Datenformat, das Sie generieren, steht unter Ihrer Kontrolle. Wenn ich nicht falsch verstanden habe, muss die Datendatei als Ganzes kein gültiger JSON sein, solange Sie sie analysieren können.
  3. Es klingt wie das einzige Problem mit Ihrer zweiten, effizienteren Methode ist die Fragilität der Aufspaltung der ursprünglich generierten JSON.

Dies deutet auf eine einfache Lösung: Statt einen riesigen JSON-Array zu erzeugen, für jedes Element Ihres Arrays einen individuellen JSON-String erzeugen - ohne Zeilenumbrüche in dem JSON-String, das heißt verwendet nur JSON.stringify(item) ohne space Argument. Verbinden Sie dann diese JSON-Zeichenfolgen mit newline (oder einem anderen Zeichen, von dem Sie wissen, dass es nie in Ihren Daten erscheint) und schreiben Sie diese Datendatei.

Wenn Sie diese Daten lesen, teilen Sie die eingehenden Daten auf der neuen Zeile auf, und führen Sie dann die einzelnen Zeilen einzeln auf diese Zeilen aus (JSON.parse()). Mit anderen Worten, dieser Schritt ist genau wie Ihre zweite Lösung, aber mit einem einfachen String-Split, anstatt sich mit den Zeichenzählungen und geschweiften Klammern zu beschäftigen.

Code könnte wie folgt aussehen (wirklich nur eine vereinfachte Version von dem, was Sie auf dem Laufenden):

var fs = require('fs'); 
var arr2 = fs.readFileSync(
    'JMdict-all.json', 
    { encoding: 'utf8' } 
).trim().split('\n').map(JSON.parse); 
:

var fs = require('fs'); 
var arr2 = fs.readFileSync(
    'JMdict-all.json', 
    { encoding: 'utf8' } 
).trim().split('\n').map(function(line) { 
    return JSON.parse(line); 
}); 

Wie Sie in einem Bearbeitungs bemerkt, können Sie diesen Code vereinfachen könnten

Aber ich würde vorsichtig sein. Es funktioniert in diesem speziellen Fall, aber es besteht eine potentielle Gefahr im allgemeineren Fall. Die JSON.parse Funktion takes two arguments: der JSON-Text und eine optionale "reviver" -Funktion.

Die [].map() Funktion passes three arguments zu der Funktion, die es aufruft: der Elementwert, Array-Index und das gesamte Array.

Also, wenn Sie JSON.parse direkt übergeben, es mit JSON Text als erstes Argument (wie erwartet) genannt, wird aber es ist auch eine Nummer für die „Erneuerer“ Funktion übergeben werden. JSON.parse() ignoriert dieses zweite Argument, weil es keine Funktionsreferenz ist, also sind Sie hier OK. Aber Sie können sich wahrscheinlich andere Fälle vorstellen, in denen Sie in Schwierigkeiten geraten könnten - es ist also immer eine gute Idee, dies zu überprüfen, wenn Sie eine beliebige Funktion übergeben, die Sie nicht in [].map() geschrieben haben.

+1

Gibt es einen Namen für 'feldgetrennte JSON'? Ich habe solche Dateien schon einmal erstellt, mit Tabs, aber immer schattig, zum Teil wegen der Hybridisierung von JSON und TSV, aber auch weil ich nie wusste, was ich diese Datei nennen soll oder welche Dateierweiterung ich benutzen soll. Ich würde es nicht JSON nennen wollen, das wird endlose Verwirrung verursachen. http://en.wikipedia.org/wiki/Line_Delimited_JSON sieht aus wie es ist eine Sache. –

+0

Das ist ein guter Punkt, Sie würden die Datei nicht als ganzes JSON bezeichnen, auch wenn jede Zeile davon ein JSON-Text ist. Ich würde jede Verlängerung wählen, die Sie mögen, oder lassen Sie mich vorschlagen: '.data': –

+1

[Linie-abgegrenztes JSON] (http://en.wikipedia.org/wiki/Line_Delimited_JSON) ist eine Sache, wer wusste! '.ldjson' oder' .ldj' ist anscheinend die Dateiendung oder ['.jsonl'] (http://jsonlines.org/). –

0

Ich denke, ein Kommentar deutete auf die Antwort auf diese Frage, aber ich werde es ein wenig erweitern. Die verwendeten 1 GB Speicher beinhalten vermutlich eine große Anzahl von Zuweisungen von Daten, die tatsächlich "tot" sind (insofern sie nicht mehr erreichbar sind und daher vom Programm nicht mehr benutzt werden), aber noch nicht von der Müllsammler.

Fast jeder Algorithmus, der einen großen Datensatz verarbeitet, erzeugt auf diese Weise sehr viel Detritus, wenn die verwendete Programmiersprache/Technologie eine typische moderne Sprache ist (zB Java/JVM, C# /. NET, JavaScript). Schließlich entfernt der GC es. Es ist interessant festzustellen, dass Techniken verwendet werden können, um die Menge der ephemeren Speicherzuweisung, die bestimmte Algorithmen eingehen (durch Zeiger in die Mitte von Strings) dramatisch zu reduzieren, aber ich denke, dass diese Techniken schwer oder unmöglich zu verwenden sind JavaScript.

Verwandte Themen