In Moment ist ein sehr gefährlicher Infekt in der PHP-Szene namens “ PhpBenchmarkItis” in Umlauf. Wenn es erwischt hat, derjenige ist gezwungen Dinge in PHP miteinander auf Leistung und Schnelligkeit gegeneinander zu prüfen. Mich hat die “ PhpBenchmarkItis” nun auch erwischt. Ich musste unbedingt erfahren welche Methode von PHP und ZF eine schnellere Verbindung zu einem Server aufbaut und dessen Antwort bzw. Ergebnis ausliefert. Hintergrund des Ganzen ist, eine Klasse zu erstellen die auf dem schnellsten Wege verschiedene RSS-Feeds auslesen und ausliefern kann.

Die Testumgebung
Server = Debian 5.0.4
Processors = Intel(R) Xeon(R) CPU E5335 @ 2.00GHz
PHP Version = 5.3.3-0.dotdeb.1
allow_url_fopen = On
cURL Version = 7.18.2
ZendFramework Version = 1.10.0

Das Test-Scenario
Als Testumgebung dient PHPUnit, da jeder Test isoliert ausgeführt werden kann. Jede Methode ruft den RSS-Feed (http://krsteski.de/feed/rss) auf dieses Blog 50-mal auf und liefert ebenso 50-mal den Inhalt zurück. Gut, dieser Blog ist eine WordPress Instanz und braucht etwas zu antworten, jedoch alles in akzeptablen Zeitrahmen. Getestet werden hier: file_get_contents, fopen, Zend_Http_Client, Zend_Feed_Reader und cURL.

< ?php
class Feed_PhpBenchmarkItis extends PHPUnit_Framework_TestCase
{
    const LOOP = 50;
    const FEED = 'http://krsteski.de/feed/rss';

    public function testZendFeed()
    {
        $beginn = microtime(true);

        $i = 0;
        while($i < self::LOOP)
        {
            $feed = Zend_Feed_Reader::importFile(self::FEED);
            $feed->getFeedLink();

            ++$i;
        }

        $dauer = sprintf('%.3f', microtime(true) - $beginn);

        echo self::LOOP." ZENDFEED Anfragen in: ".
                $dauer." Sek. durchschnittlich ".
                ($dauer / self::LOOP)." Sek. pro Anfrage".PHP_EOL;
    }

    public function testFILE_GET_CONTENTS()
    {
        $beginn = microtime(true);

        $i = 0;
        while($i < self::LOOP)
        {
            $contents = file_get_contents(self::FEED);

            ++$i;
        }

        $dauer = sprintf('%.3f', microtime(true) - $beginn);

        echo self::LOOP." FILE_GET_CONTENTS Anfragen in: ".
                $dauer." Sek. durchschnittlich ".
                ($dauer / self::LOOP)." Sek. pro Anfrage".PHP_EOL;

    }

    public function testFOPEN ()
    {
        $beginn = microtime(true);

        $i = 0;
        while($i < self::LOOP)
        {
            $handle = fopen(self::FEED, "r");
            $contents = stream_get_contents($handle, -1);
            fclose($handle);

            ++$i;
        }

        $dauer = sprintf('%.3f', microtime(true) - $beginn);

        echo self::LOOP." FOPEN Anfragen in: ".
                $dauer." Sek. durchschnittlich ".
                ($dauer / self::LOOP)." Sek. pro Anfrage".PHP_EOL;

    }

    public function testCURL()
    {
        $beginn = microtime(true);

        $i = 0;
        while($i < self::LOOP)
        {
            $handle = curl_init(self::FEED);
            curl_setopt($handle, CURLOPT_HEADER, false);
            curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
            $contents = curl_exec($handle);
            curl_close($handle);

            ++$i;
        }

        $dauer = sprintf('%.3f', microtime(true) - $beginn);

        echo self::LOOP." CURL Anfragen in: ".
            $dauer." Sek. durchschnittlich ".
            ($dauer / self::LOOP)." Sek. pro Anfrage".PHP_EOL;
    }

    public function testZendHttpClient()
    {
        $beginn = microtime(true);

        $i = 0;
        while($i < self::LOOP)
        {
            $urlClient = new Zend_Http_Client(self::FEED);
            $contents = $urlClient->request()->getRawBody();

            ++$i;
        }

        $dauer = sprintf('%.3f', microtime(true) - $beginn);

        echo self::LOOP." ZEND_HTTP_CLIENT Anfragen in: ".
            $dauer." Sek. durchschnittlich ".
            ($dauer / self::LOOP)." Sek. pro Anfrage".PHP_EOL;
    }
}


Das Testergebnis

50 ZENDFEED Anfragen in: 4.007 Sek. durchschnittlich 0.08014 Sek. pro Anfrage
50 FILE_GET_CONTENTS Anfragen in: 4.016 Sek. durchschnittlich 0.08032 Sek. pro Anfrage
50 FOPEN Anfragen in: 6.295 Sek. durchschnittlich 0.1259 Sek. pro Anfrage
50 CURL Anfragen in: 3.564 Sek. durchschnittlich 0.07128 Sek. pro Anfrage
50 ZEND_HTTP_CLIENT Anfragen in: 3.999 Sek. durchschnittlich 0.07998 Sek. pro Anfrage
Time: 23 seconds, Memory: 9.75Mb

Wie erwartet ist hier der Aufruf mit cURL schneller. Bei cURL habe ich jedoch das Problem, dass ich hier nicht genau weiß ob es sich um ein valides RSS-Feed handelt. Zudem muss ich das Ergebnis das cURL zurück liefert, irgendwie parsen um es weiter verarbeiten zu können. Diese Logik muss also zusätzlich implementiert werden. Ich habe mich für Zend_Feed_Reader entschieden und verzichte gerne auf bisschen Zeit und bekomme dafür mehr Sicherheit. Zudem der Zend_Feed_Reader kann das RSS-Feed validieren, löst entsprechenden Exceptions falls es irgendwelche Probleme mit dem RSS-Feed gibt und liefert das Ergebnis als eine DOMDocument Instanz zurück. Und das ist ja spitze! Anhand der DOMDocument Instanz kann ich nun ganz bequem über den Datenbestand iterieren.

10 thoughts on “cURL vs. Fopen vs. ZF

    1. @Web-Rocker
      Hi Marco. Die variable Netzwerklatenz habe ich ebenfalls ausser Acht gelassen. Das Script wurde auf einem entfernten Server ausgeführt.- C.R.A.N.E. :-)

  1. ..ob das so aussagekräftig ist als Benchmark möchte ich bezweifeln. Ich könnte mir vorstellen, wenn man den Feed auch bei curl, fopen, etc. hinterher per DOMDocument parsen lässt, sehen die Unterschiede anders aus.

  2. Dein Zend_Feed-Test ist absolut nichtssagend. Zend_Feed_Reader::importFile($feed) beruht auf file_get_contents (http://framework.zend.com/svn/framework/standard/trunk/library/Zend/Feed/Reader.php). Außerdem verarbeitet Zend_Feed deinen Feed, während es die anderen nicht tun.
    Zend_HTTP_Client wiederum beruht auf stream_socket_client() und fwrite() und fread().
    Desweiteren musst du noch die durchschnittliche Verbindungsdauer abziehen, die ja bei allen ähnlich ist. Und dann hast du ein Ergebnis, bei dem es vollkommen irrelevant ist, welche Verbindungsart du verwendest.
    Benchmarks sind in der Regel schrott, da erstens meist Äpfel mit Birnen verglichen werden, zweitens die Hälfte der Testbedingungen außer Acht gelassen wird und drittens dann das Ergebnis keinen nennenswerten Unterschied liefert, sodass das nur Erbsenzählerei ist.

    1. @Laberkopp
      vielen Dank für deinen Kommentar. Deiner Kritik stimme ich vollkomen zu. Es ist jedoch so: Bevor ich ZF Komponenten verwende, schaue ich sie mir genau an. Ich habe mich für den Zend_Feed_Reader entschieden, weil er dafür sorgt, dass die RSS-Feed Conventionen eingehalten werden und einem die Möglichkeit gibt, sofort auf die Einträge oder Kategorien etc. zuzugreifen. Die durchschnittliche Verbindungsdauer habe ich dabei leider außer acht gelassen, würde mich aber sehr freuen, wenn du hier ein kleines Beispiel dafür als Kommentar posten könntest, wie man eine durchschnittliche Verbindungsdauer bestimmen kann.

  3. Naja, sicher ist es richtig, das zu nehmen, was am besten passt. Dann solltest du aber auch nicht das in einen Vergleich mit Funktionen einbeziehen, die wirklich nur die Seite abrufen. Außerdem verwendest du den Zend_Feed_Reader falsch. Zend_Feed_Reader::importFile($file) ermöglicht zwar Urls, allerdings nur aufgrund der Tatsache, dass file_get_contents auch Urls erlaubt. Richtig wäre hingegen die Verwendung von Zend_Feed_Reader::import($url), die dann zwar einen Vergleich unmöglich macht (Caching), aber im produktiven Einsatz richtig wäre.
    Durchschnittliche Verbindungsdauer – beispielsweise via wget. Da erhalte ich jetzt bei einigen Abfragen (natürlich nicht representativ) eine Dauer von 0,06s/0,05s. Ziehst du die jetzt bei jedem deiner Werte ab, erkennt man schnell die Sinnlosigkeit des Vergleiches.
    Wie gesagt – ich halte nicht viel von Benchmarks, da die Testbedingungen idR schlecht sind und ein Ergebnis herauskommt, bei dem es um Millisekunden und weniger geht – die der Benutzer niemals bemerken wird.
    Wenn es dir um einen Vergleich von Feedreadern geht – warum schreibst du nicht einen Vergleich von Feedreadern?

  4. Eventuell ist der Einsatz von curl_multi* interessant wenn man parallel viele Feeds downloaden möchte. Oder aber Job-Queues wie Gearman etc., damit kann man prima parallelisieren und die Leitung auch auslasten. Der höhere Aufwand lohnt sich natürlich erst ab einigen hundert Feeds und wenn die serielle Laufzeit zu lang dauert.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

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