PHP, Objekte, Schleifen, Benchmark

Vor einigen Tagen ist mir ein Code-Abschnitt begegnet, über den ich mich ein wenig gewundert habe. Interessanterweise handelte es sich dabei um ein paar geänderte Zeilen, die im Diff zu sehen waren. 

Bis dato existierte der Code wie in folgendem Beispiel ersichtlich:

public function run() {
   
      $someClass = new Someclass();
      
      for ($i = 0; $i < self::RUNS; $i++) {
         $someClass->doSomething();
      }
      echo $someClass->echoSomething();
   }

Eine einfache Geschichte – auf einem Objekt wird irgend eine Aktion ausgeführt. So weit, so gut.

Derselbe Abschnitt nach der Änderung:

public function run2() {
    for ($i = 0; $i < self::RUNS; $i++) {
        $this->getSomeClass()->doSomething();
    }
    echo $this->getSomeClass()->echoSomething();
   
}

Neu hinzu gekommen war die Methode getSomeClass(), die mittels Factory-Klasse die entsprechende Objektinstanz erzeugt. Dagegen ist grundsätzlich nichts einzuwenden, das Design Pattern der Factory verspricht die Entkopplung der Instanziierung von der Verwendung im Client. Insofern könnten z.B. per Parameterübergabe, per Konfiguration o.ä. unterschiedliche Objekte erzeugt werden. Was aber, wenn das gar nicht benötigt wird, sondern in der dargestellten Schleife tatsächlich auf ein- und dasselbe Objekt zugegriffen werden soll?

Zudem benutzt die getSomeClass()-Methode ein Singleton, so dass tatsächlich immer dieselbe Instanz zurück gegeben, wird, sofern die Factory-Methode zur Erzeugung bereits aufgerufen und das Objekt erzeugt wurde. Die Instanziierung geschieht somit nur ein einziges Mal. Was wiederum auch nicht verwundert, denn im vorherigen Code wurde einfach im Konstruktur die benötigte Instanz erzeugt – und darauf gearbeitet.

Das brachte mich dann auf die Idee, den bisherigen und neuen Code einfach mal gegenüber zu stellen. Bei 1.000.000 Durchläufen ergibt sich folgendes Ergebnis (in Sekunden):

Bisherige Implementierung: run: 1.4776

Mit zusätzlichem Methodenaufruf: run2: 2.6802

Wie erwartet verursacht der Aufruf der getSomething()-Methode zusätzlichen Overhead. Zwar wird angesichts des Singleton-Patterns die Factory-Methode nur einmal verwendet, aber allein der Methodenaufruf von getSomething() mit anschließender Abfrage, ob die Instanz bereits existiert und entsprechender Rückgabe sorgt für eine nahezu Verdoppelung der Laufzeit. Ich habe es mehrfach getestet, immer mit sehr ähnlichen Ergebnissen. Zum Vergleich habe ich daraufhin noch eine dritte Variante getestet, die zwar die Factory nutzt, aber innerhalb der Schleife mit demselben Objekt arbeitet. Wie ebenfalls erwartet, ist die Laufzeit dabei wieder ähnlich der ursprünglichen Implementierung.

Nun könnte man entgegnen, dass sicherlich die Schleife nicht sehr oft durchlaufen wird – im konkreten produktiven Code wären es vielleicht 10 Mal pro Skriptaufruf. Damit wären die Unterschiede insgesamt marginal und vermutlich nicht bedeutsam. Natürlich ist das richtig, nur wird bei einer hoch frequentierten Seite der betreffende Abschnitt vielleicht sehr häufig ausgeführt. Schon haben wir wieder die Situation, dass eine hohe Zahl von unnötigen Methodenaufrufen existiert, allein das Verhältnis ist nicht ganz so extrem wie im hier verwendeten Codebeispiel.

So veranschaulicht das Beispiel meiner Ansicht nach doch, wie sich durch sehr einfache Ansätze – von “Optimierung” wage ich an dieser Stelle gar nicht zu reden – ein paar Millisekunden einsparen lassen. Die Server – und in letzter Konsequenz User – werden es einem danken.

Hier noch das komplette Codebeispiel zum Einblick, für eigene Tests o.ä:

<?php

class SomeClass
{

   private $i = 0;

   public function doSomething() {
      $this->i++;
   }


   public function echoSomething() {
      echo $this->i . "\n";
   }   
}

class SomeClassFactory
{
   public function factory() {
      return new SomeClass();
   }
}

class MainClass
{

   const RUNS = 1000000;

   private $someClass = null;

   public function run() {
   
      $someClass = new Someclass();
      
      for ($i = 0; $i < self::RUNS; $i++) {
         $someClass->doSomething();
      }
      echo $someClass->echoSomething();
   }         
   
   public function getSomeClass() {
      if ($this->someClass == null) {
         $someFactory = new SomeClassFactory();
         $this->someClass = $someFactory->factory();
      }
      return $this->someClass;
   }
   
   public function run2() {
     for ($i = 0; $i < self::RUNS; $i++) {
         $this->getSomeClass()->doSomething();
      }
      echo $this->getSomeClass()->echoSomething();
   
   }
   
    public function run3() {
        $someClass = $this->getSomeClass();
        for ($i = 0; $i < self::RUNS; $i++) {
            $someClass->doSomething();
        }
        echo $someClass->echoSomething();
                            
   }
         
}

$main = new MainClass();

$timeStart = microtime(true);
$main->run();
$timeEnd1 = microtime(true);

$main->run2();

$timeEnd2 = microtime(true);

$main->run3();

$timeEnd3 = microtime(true);

echo "Result: ";
echo "run: " .  number_format($timeEnd1 - $timeStart,4) . "\n";
echo "run2: " . number_format($timeEnd2 - $timeEnd1,4) . "\n";
echo "run3: " . number_format($timeEnd3 - $timeEnd2,4) . "\n";

Disclaimer: Ja, beim dritten Durchlauf wird dasselbe Objekt genutzt wie beim zweiten. Die Implementierung einer Reset-Methode, um das Objekt zu entfernen habe ich mir für das Beispiel erspart. Man möge es mir verzeihen, im Real Life würden die Klassen ja auch nicht in derselben Datei stehen… 😉

 

Schreibe einen Kommentar

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

Tags:
Kategorien: PHP Programmierung