Freaky Friday: Ein Generator für alle

Erst im letzten Artikel habe ich dazu aufgefordert weniger Arrays zu benutzen. Heute setze ich noch einen drauf und zeige ein weiteres Beispiel, warum Arrays nicht immer gut sind.

Ganz abseits von Skalierbarkeit und Typsicherheit belegen Arrays hin und wieder auch jede Menge Speicher. Gerade in den letzen Monaten fiel es mir besonders bei der Umsetzung von BiPRO Webservices auf. Transfer Services nach BiPRO Norm 430 oder Listen Services nach BiPRO Norm 480 können schon eine ganz schön große Datenmenge umfassen. Obwohl die Serverleistung in den letzten Jahren stetig zugenommen hat, heißt das ja noch lange nicht, dass wir deren Leistung und Kapazität bis zum letzten Byte ausreizen müssen. Viel eher gilt auch heute noch das Gebot so performant und resourcenschonend zu arbeiten, wie es eben nur geht. Gerade die aufkommende Bewegung Developers for Future hat den Anspruch über den eigenen Code mal im Sinne des Klimas nachzudenken. Viel Speicherverbrauch bedeutet auch immer einen höheren Stromverbrauch.

Seit PHP 5.5 bietet PHP so genannte Generatoren an. Generatoren sind eine einfache, elagante Möglichkeit Iteratoren zu erzeugen, ohne diesen komplexen Overhead einer dafür notwendigen Klasse, die das Iterator Interface implementiert. Ein Generator ermöglicht also die Iteration über eine Datenmenge, ohne dabei ein Array im Speicher zu erzeugen. Ohne ein solches Array gibt es also keine Möglichkeit des Speicherüberlaufs oder Probleme mit der Laufzeit, die das Prozessieren des Arrays benötigen würde. Das Resultat ist eine simple foreach-Iteration.

Hierzu mal ein herkömmliches Beispiel, indem wir ein Array mit 1.000.000 Einträgen erzeugen. Mit der Funkion memory_get_peak_usage ermitteln wir alle 100.000 Einträge den Speicherverbrauch dieser Funktion.

function generateValues()
{
    $values = [];
    echo round(memory_get_peak_usage() / 1024 / 1024, 2) . 'MB' . PHP_EOL;

    for ($i = 0; $i <= 1000000; $i++) {
        $values[] = $i;

        if (($i % 100000) == 0) {
            echo round(memory_get_peak_usage() / 1024 / 1024, 2) . 'MB' . PHP_EOL;
        }
    }

    return $values;
}

$values = generateValues();
foreach ($values as $value) {}

Wie ihr seht, handelt es sich also um eine ganz normale Funktion, die ein Array mit einer Million Einträgen erzeugt. Auf meiner lokalen Maschine erzeugt diese Funktion folgende Ausgaben.

0.36 MB 
8.33 MB 
14.33 MB 
26.33 MB 

Okay, kann man mal machen. Die angezeigten 26.33 MB, die das Array an Speicher verbraucht, klingen in Zeiten von Arbeitsspeichern in Gigabyte Größe wirklich nicht viel. Wenn man aber mal genauer drüber nachdenkt, sind 26,33 MB für eine Million Werte schon ein bisschen übertrieben.

Genau hier setzen Generatoren an. In diesem Zusammenhang wurde das yield Schlüsselwort eingeführt, welches pro Iteration ein entsprechendes Generator Objekt zurück gibt. Ändern wir einfach mal die obige Funktion, indem wir Generatoren nutzen.

function generateValues()
{
    echo round(memory_get_peak_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
    for ($i = 0; $i <= 1000000; $i++) {
        yield $i;
        
        if (($i % 100000) == 0) {
            echo round(memory_get_peak_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
        }
    }
}

$values = generateValues();
foreach ($values as $value) {}

Auf den ersten Blick fällt sofort auf, dass wir kein Array mehr erzeugen. Weiterhin gibt es auch kein return mehr in der Funktion. Lediglich das angesprochene yield Schlüsselwort wird in der for-Schleife verwendet. Hier zeigt sich auch der Vorteil der yield-Syntax. Obwohl es ähnlich wie return funktioniert, kann weitere Logik nach einem yield angegeben werden.

Durch das yield wird die Funktion erstmal nicht eine Million mal ausgeführt. Erst, wenn wir beginnen per foreach-Schleife zu iterieren, schreitet die Funktion zum nächsten Zahlenwert voran. Ein riesiger Vorteil in Sachen Speicherverbrauch, denn so wird der Speicher konstant mit einem Generator Objekt, welches in diesem Fall einem Array Eintrag aus der zuvor gezeigten Funktion entsprechen würde, belegt. Der Speicherverbrauch bleibt also konstant und so minimal wie möglich.

0.36 MB 
0.36 MB 
0.36 MB 
0.36 MB

Beeindruckend, oder?

Fazit

Ich selbst habe mich lange Zeit schwer getan Generatoren zu verstehen und entsprechende Szenarien zu erkennen, in denen ich Generatoren einsetzen kann. Am Ende eines Tages lohnt es sich aber mal genauer über den Einsatz von Generatoren nachzudenken. Gerade beim Abarbeiten von großen Datenmengen ergeben Generatoren mit Blick auf den Speicherverbrauch Sinn. Ich für meinen Teil werde zukünftig öfter Generatoren verwenden. Wer freut sich bitte nicht über eine performante, Resourcen schonende Anwendung?

2 Gedanken zu „Freaky Friday: Ein Generator für alle“

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.