MVCLite scheint wirklich leichtgewichtig zu sein
Gestern war es mal wieder so weit und ich wollte ein wenig mehr Performance aus MVCLite rauskitzeln. Der Ansatz war banal wie auch effektiv: Ich speichere ein Objekt einfach im RAM. Dazu habe ich die APC-Funktionen apc_store zum Ablegen und apc_fetch zum Laden der Daten genutzt. Die Basis sah folgendermaßen aus:
abstract class MVCLite_Core
{
public function getIdentifier ()
{
// gibt den Klassennamen zurück
}
public function __destruct ()
{
apc_store($this->getIdentifier(), $this);
}
public static function find ($identifier)
{
$result = apc_fetch($identifier);
if($result === false) { throw new Exception(); }
return $result;
}
}
Damit wird das Objekt transparent via APC im RAM gespeichert. Zum Holen wird die statische find-Methode genutzt, die eine Exception wirft, wenn das Objekt nicht vorhanden ist. Somit ist es relativ einfach möglich, zwischengespeicherte Objekte zu benutzen.
class Foobar extends MVCLite_Core
{
}
try
{
$foobar = MVCLite_Core::find('Foobar');
}
catch (Exception $e)
{
$foobar = new Foobar();
}
Es macht also keinen Unterschied, mit welcher Version wir
letztendlich arbeiten, außer, dass eine Version aus dem RAM kommt.
Natürlich ist dieses Schreiben via APC im Ggs. zum Erstellen von
Objekten sehr zeitaufwändig, aber bei großen Objekten lohnt es sich
definitiv.
Um zu schauen, ob es sich überhaupt lohnt, habe ich ein kleines
Benchmark-Skript angefertigt, das eine grobe Tendenz zeigen soll.
// die instanzierte Klasse (Foobar.php)
class Foobar
{
public $array = array();
public function __construct ()
{
for($i = 0; $i < 100; $i++)
{
$this->_array[] = md5($i);
}
}
}
// Benchmark Teil 1
define('INTERVAL', 10);
require 'Foobar.php';
$start = microtime(true);
$counter = 0;
do
{
$tmp = apc_fetch('Foobar');
if($tmp === false)
{
$tmp = new Foobar();
}
apc_store('Foobar', $tmp);
$counter++;
}
while(microtime(true) - $start < INTERVAL);
echo "Iterations: $counter/" . INTERVAL . "s
";
apc_delete('Foobar');
// Benchmark Teil 2
$start = microtime(true);
$counter = 0;
do
{
$tmp = new Foobar();
$counter++;
}
while(microtime(true) - $start < INTERVAL);
echo "Iterations: $counter/" . INTERVAL . "s";
Ich generiere hierbei eine bestimmte Anzahl von Hashes um den Aufwand der Initialisierung zu simulieren. 10s lang wird überprüft, wie viele Iterationen in dem Zeitraum möglich sind. Natürlich ist es undenkbar, die genaue Anzahl von Iterationen in dem Zeitraum herauszufinden - zumindest mit einem gerechtfertigten Aufwand -, aber mein Ziel ist ja ganz klar die Tendenz.
- 100 Hashes
- APC: 58.378/10s
- Instanzierung: 12.550/10s
- 10 Hashes
- APC: 142.968/10s
- Instanzierung: 106.351/10s
- 1 Hash
- APC: 150.731/10s
- Instanzierung: 429.303/10s
Bereits bei 10 Hashes lohnt sich die Nutzung von APC. Wie gesagt, es
ist nur eine Tendenz. Als ich dann die bereits beschriebene
Implementation in MVCLite integriert hatte, habe ich
ApacheBench darüber laufen lassen und siehe da, es ist sogar langsamer
als mit der direkten Instanzierung! Was heißt das? Es bedeutet, dass MVCLite sein Ziel, eine leichtgewichtige
Implementation zu bieten, nicht verfehlt hat und darüber bin ich
glücklich. Derzeit brauche ich also keinen internen Mechanismus zum
Zwischenspeichern im RAM, zumindest nicht im Kern. Für das
Zwischenspeichern außerhalb des Kerns werde ich wahrscheinlich Zend_Cache benutzen, da man damit komplexere Mechanismen sehr leicht umsetzen kann.
Allerdings gab es neben der Verringerung der Geschwindigkeit auch noch einige andere Probleme mit APC. Z.B. speichert APC normale Objekte nur in der Tiefe von 1,
was heißt, dass komplexere Datenstrukturen nicht effektiv genug
gespeichert werden können. Die vorgeschlagene Lösung, die Objekte zu serialisieren
ist okay, aber nicht optimal. Denn dadurch verliere ich die Referenzen
und ein Objekt existiert mehrfach. Darunter leidet natürlich die
Robustheit von MVCLite, was ich tunlichst vermeiden
will. Der Code müsste dafür umgeschrieben werden, worunter natürlich
die Transparenz leiden würde. Eine Lösung könnte das Speichern des
gesamten Kerns in einer Variable sein. Man könnte dann die gesamten
auftretenden Referenzen suchen und eliminieren, sodass dieselben
Objekte nur an einer Stelle auftauchen. Beim Laden der gesamten
Applikation muss es dann natürlich möglich sein, diese Referenzen
wieder herzustellen. Aber dieses Prozedere ist ja anscheinend nicht
notwendig...
Fazit? MVCLite ist auf einem guten Weg, aber ich werde mich sicherlich nicht ausruhen, sondern versuchen, exzessiv auf das Konzept der Lazy Initialization setzen, um unnötige Instanzierungen zu vermeiden. Dieses Konzept wird bereits in MVCLite_Db_PDO eingesetzt, damit eine Verbindung nur dann erstellt wird, wenn sie wirklich benötigt wird. Auch in der View-Helper-Registry ist dieses Konzept zu finden.