Performancevergleich am Beispiel von nl2p
Saturday, 21. March 2009
Die Versionen 0.1 und 0.2 von nl2p sind funktional (fast) gleichwertig, aber in einem elementar wichtigen Punkt unterschiedlich: Der Performance.
In meinem Testblog werden die letzten 20 Einträge dieses Blogs gespiegelt und ein paar zusätzliche Testfälle angezeigt, die Seite 2 war also fast identisch. Ohne das Plugin dauert das Laden der zweiten Seite drei Sekunden. Mit Version 0.2 braucht es circa vier, diese kleine Verzögerung ist eigentlich normal. Aber mit Version 0.1 waren es ganze 16 Sekunden.
Ein kleines Beispiel:
/** * Make sure none of these block_elements are within a <p> * @param string text * @return string * */ function isolate_block_elements($text) { $block_elements = array("<table","<ul","<ol","<pre", "<dir", "<dl", "<h1", "<h2", "<h3", "<h4", "<h5", "<h6", "<menu", "<blockquote"); foreach ($block_elements as $start_tag) { for ($i=0; $i<count($text); $i++) { if ($this->contains($i, $text, $start_tag) === true) { $text[$i] = "</p>" . $text[$i]; } if ($this->contains($i, $text, $this->end_tags($start_tag)) === true) { $text[$i+strlen($this->end_tags($start_tag))] = $text[$i] ."p>"; } } } return $text; }
An sich sollte das halbwegs vernünftig aussehen. Für jedes Blockelement werden alle Arrayelemente überprüft. Aber da fängt es an: $text ist ein Array, das mittels str_split() aus einem String erstellt wurde. Jedes tag ist also zersplittert, $text[0] erhält z.B. das '<', $text[1] das 'u' von <ul> und so weiter. Das muss aber nicht so sein: Da die p-tags in ein Arrayindex reingesetzt werden, kann ein Arrayelement auch einen String und nicht nur einen Buchstaben enthalten. Die Funktion contains() überprüft also so viele Arrayelemente, wie das Blockelement lang ist, jeweils doppelt:
function contains($start, $text, $tag) { $tag_array = str_split($tag); $tag_cursor = 0; $scattered = true; for ($i=$start; $i < $start + strlen($tag); $i++) { if (strpos($text[$i], $tag) !== false) { return true; } if ($text[$i] !== $tag_array[$tag_cursor]) { $scattered = false; } $tag_cursor++; } return $scattered; }
Grausamer, grausamer Code. Jedes Arrayelement wird im Zusammenspiel der beiden Funktionen pro Blockelement mindestens sechsmal angefasst, noch öfter, wenn das Blockelement länger ist. Dass '$start + strlen($tag)' (wie auch das count($text) in der Funktion oben) bei jedem Schleifendurchlauf durchgeführt wird kommt noch dazu und unterstreicht, dass das schlecht gewachsener, wuchernder Code ist, der unbedingt neu strukturiert werden musste.
In Version 0.2 gibt es kein contains(). Stattdessen arbeiten alle Funktionen außer dem Grundparser nicht mit Arrays, sondern direkt auf dem String. Die 'isolate_block_elements()' erfüllt noch die gleiche Aufgabe, aber wesentlich schneller:
function isolate_block_elements($textstring) { $block_elements = array("<table","<ul","<ol","<pre", "<dir", "<dl", "<h1", "<h2", "<h3", "<h4", "<h5", "<h6", "<menu", "<blockquote"); $block_elements_amount = count($block_elements); for($i=0;$i<$block_elements_amount;$i++) { $start_tag = $block_elements[$i]; //first see if block-element really exists $tag_position = strpos($textstring, $start_tag); if ($tag_position === false) { continue; } else { $end_tag = $this->end_tags($start_tag); $textstring = str_replace("$start_tag", "</p>$start_tag", $textstring); $textstring = str_replace("$end_tag", "$end_tag<p>", $textstring); } } return $textstring; }
$text ist hier ein String, kein Array, die Umbenennung in $textstring mag suboptimal sein, markiert das aber. Das contains() wurde mit strpos() ersetzt. Das Einfügen der p-Tags erfolgt nicht mehr manuell mithilfe einer for-Schleife, sondern mittels 'str_replace()'.
Ich gehe davon aus, dass nichtmal die zweite Version optimal ist. Immerhin bin ich kein geübter, geschweige denn gelernter PHP-Programmierer. Aber sie ist wesentlich effizienter als die erste.
Der Vergleich soll ein Beispiel dafür sein, wieviel grausamer Code durch fehlende Distanz entstehen kann. Einen Schritt zurückzutreten und die Grundbedingung überprüfen (warum arbeitet die Funktion auf einem Array?) hilft enorm. Auch ist es ein Plädoyer für Teamarbeit: Hätte ich mit einem meiner Kollegen zusammengearbeitet, wäre durch das gemeinsame Überlegen schon der erste Versuch wahrscheinlich wesentlich besser ausgefallen.