PHP::serialize() - Zu hoher Speicherverbrauch - Alternativen?
Raketenbastler
- php
Situation:
Ich hatte einen echt großen Array, den ich serialisiert gespeichert und gelesen habe.
Zuletzt war er ~48 MB groß. Beim Speichern beschwerte sich PHP wegen der Überschreitung des Speicherlimits von 128MB - und zwar genau in der Zeile, wo
file_put_contents( $file, serialize( $BigArray) );
drin stand. Der übrige Speicherverbrauch (außer dem Array) ist vernachlässigbar.
Das Problem mit dem aktuellen Skript habe ich mit einem Technologiewechsel gelöst (↦ squlite3). Und bin zufrieden.
Frage in die Runde:
$BigArray = unserialize( file_get_contents( $file ) );
war sehr schnell.
Gibt es womöglich eine speichersparende Methode um solche Datenmengen für Systeme ohne Voraussetzungen (wie z.B. eine Datenbank) wegschreiben zu können?
Hinweise:
unserialize()
hat dieses speicherfressende Verhalten nicht, die auf einem System mit 256MB Limit erzeugte Datei konnte ich problemlos (und sehr schnell!) einlesen.json_encode()
meine ich nicht, weil json_decode()
bei solchen Datenmengen spürbar langsamer wird.unserialize()
einlesen kann.Hello,
hat das Array eine Satzstruktur, also wiederkehrende Semantik?
Dann könntest Du die Datensätze einzeln serialisieren und wegschreiben. Nach jedem Satz ein RS (0x1E) oder ETB (0x17) einfügen. Damit kannst Du dann nachher beim Lesen auch wieder bequem vorspulen, um die Sätze wieder einzulesen.
Glück Auf
Tom vom Berg
Hallo Felix,
nein gzipencode bringt leider nichts.
Ich hab mir die serialisierten Daten mal angesehen. Es müsste wohl etwas sein, was den Array quasi von innen nach außen direkt in eine mit fopen() geöffnete Datei wegschreibt und mit rewind(), fseek() und Schwestern den Dateizeiger hin- und hersetzt.
Grund: serialize()
notiert Datentyp, Länge bei (Sub-)Arrays und bei Strings die Länge (in Bytes, nicht Zeichen) des jeweiligen Elements (und von dessen Kindern) vor demselben. Deshalb kann unserialize()
mit ein wenig Zahlenzauberei wahlfrei auf die Daten zugreifen und so irre schnell sein. JSON tut das nicht, muss sequentiell durch die Daten gehen und ist deswegen beim Lesen langsamer.
Beim Erzeugen muss man halt anders herum, also von innen nach außen arbeiten. Und wenn der Speicher nicht reicht, dann muss man halt direkt in die Datei schreiben. Mal hinten, mal vorn…
Hallo Raketenbastler,
Tom ist auf dem richtigen Weg, es geht aber einfacher. Man kann ein Array relativ einfach elementweise serialisieren und dabei die String-Struktur beibehalten, die serialize() erzeugt. Dann kann man stumpf mit unserialize() wieder einlesen. Wenn auch unserialize platzt, könnte man eine Funktion stream_unserialize schreiben. Dafür muss man allerdings noch ein bisschen mehr das Serialisierungsformat von PHP erforschen.
Ein serialisiertes Array beginnt mit "a:n:{" und endet mit "}". n ist die Anzahl der Elemente. Dazwischen finden sich die Keys und Values des Arrays. Ein skalarer Wert wird dabei mit einem ; abgeschlossen, ein Arraywert nicht. Das muss einen aber nicht kümmern, das erledigt die PHP serialize()-Funktion automagisch.
Das folgende Beispiel zeigt es mit echo, statt dessen kannst Du auch fwrite nehmen um in eine Datei zu schreiben. Oder einen Callback übergeben, der sich um das Speichern der serialisierten Fragmente kümmert.
Für eine produktionsreife Version müsste man auch noch prüfen, welcher Typ übergeben wurde, und dann einen passenden Streamserializer dafür verwenden.
function stream_serialize($arr) {
echo "a:" . count($arr) . ":{";
foreach ($arr as $k => $v) {
echo serialize($k).serialize($v);
}
echo "}";
}
Wenn man statt des Echo die Teilergebnisse in einem String sammelt, muss man String-Operationen ausführen. Das Verketten zweier Strings benötigt doppelten Speicher, weil man die beiden Originalstrings braucht und dazu Speicher für den Ziel-String. Hinzu kommt das $bigArray. Beim Deserialisieren braucht man nur den Gesamtstring, den man aus der Datei gesaugt hat, und das Array. Ein Array vergrößert sich sparsamer als ein String, weil man dabei nur eine Liste von Zeigern auf Arrayeinträge vergrößeren muss.
Es kann auch sein, dass die wiederkehrenden Operationen mit großen Strings viel Zeit gekostet haben. Wenn man Eintrag für Eintrag serialisiert, könnte es unter dem Strich schneller sein. Keine Ahnung, muss man testen.
Rolf
Ich hab noch mal drüber geschlafen und die Idee beerdigt.
Grund: Wenn PHP bei 128MB Speicherlimit beim serialisieren eines Arrays aussteigt, sobald der resultierende String rund 49MB bzw. 38% des Limits erreicht, dann kann ich im Prinzip machen was ich will - werden die Datenmengen größer (und genau das ist deren innerste Natur), so tritt das nur scheinbar gelöste Problem ohnehin bald auch beim Einlesen auf. Spätestens wohl bei 50%, also 64MB, also 25MB mehr oder bei einem Wachstum um lasche 50%. Auf deutsch: vielleicht schon morgen früh.
Einzige wirkliche Lösung: „Externisierung“ der Datenhaltung. Im konkreten Anwendungsfall also Datenbank (sqlite3, weil man dann die Datei an sich „schubsen“ und anderswo oder anderweitig weiterverarbeiten kann).
Weil mir auch nichts anderes mehr einfällt, warum ich etwas aufwendiges und fehleranfälliges wie ein serialize_to_file( $file, $arr )
schreiben sollte, war die Beerdigung der Schnapsidee fällig.
Hello,
dass das iterative bzw. rekursive Erzeugen von PHP-Arays gegenüber dem Lesezugriff auf diese erheblich viel mehf Zeif benötigt, haben wid ja neulich schon mal diskutiert. Nur file()
ist schnell.
Mein Versuch mit der Speicherung in einer direktgestreuten Struktur steht dazu noch aus.
Ich möchte aber nochmal daran erinnern, dass die Speicherung der Daten als "Spaltearray" viel Speicherplatz sparen kann. Voraussetzung dafür ist selbstverständlich, dass die Daten einen derartigen Zusammenhang haben und keine verzweigte Baumstruktur benötigen.
Mein Artikel dazu ist aber wohl gelöscht. Zumindest finde ich ihn nicht mehr.
Glück Auf
Tom vom Berg
Ihr hab Euch Mühe gegeben und nachgedacht. Und ich war so unhöflich, mich nicht zu bedanken.
Das sei hiermit nachgeholt - also Dank an Felix, Rolf, TS (in alphabetischer Reihenfolge) und auch an jene, die nachgedacht aber nichts geschrieben haben.