Vor ein paar Tagen berichtete ragtek davon, dass count() im Schleifenkopf unschön und ineffizient ist. Beidem kann ich nur zustimmen.
Aber warum ist das so?
Die Erklärung dazu ist einfach. Bei jedem Durchlauf wird count() aufgerufen und darüber die Anzahl der Elemente ermittelt. Überprüfen kann man das Verhalten mit einem kleinen Code Snippet:
<?php
class ArrayTest implements Countable
{
public $intCountCall = 0;
public function count()
{
$this->intCountCall++;
return 100000;
}
}
$objArrayTest = new ArrayTest();
$intStart = microtime(true);
for ($intI = 0; $intI < count($objArrayTest); $intI++)
{
}
echo(microtime(true) - $intStart . " Sekunden\n");
echo($objArrayTest->intCountCall . " Aufrufe von count()");
Ergebnis:
0.069180965423584 Sekunden 100001 Aufrufe von count()
Bei näherer Betrachtung scheint es also logisch, dass diese Variante langsamer ist, als wenn man count() einmal aufruft und den Wert zwischenspeichert.
Ist foreach schneller?
In den Kommentaren wird erwähnt, dass die Verwendung von for und count() unnötig ist, da es ja foreach gibt. Aber ist es auch schneller?
Schauen wir uns erst einmal zwei Schleifen mit count() an.
<?php
$arrTest = range(1, 100000);
$intStart = microtime(true);
for ($intI = 0; $intI < count($arrTest); $intI++)
{
}
echo( microtime(true) - $intStart . " Sekunden\n");
// ------------------------------------------------
$intStart = microtime(true);
$intCount = count($arrTest);
for ($intI = 0; $intI < $intCount; $intI++)
{
}
echo(microtime(true) - $intStart . " Sekunden\n");
Beide Schleifen tun dasselbe, jedoch mit dem Unterschied, dass bei der Zweiten der Wert von count() zwischengespeichert wird.
Ergebnis:
0.021790027618408 Sekunden 0.0045440196990967 Sekunden
Das Ergebnis ist so, wie wir es erwartet haben. Die zweite Schleife ist vier- bis fünfmal schneller, als die erste Variante.
Nun machen wir das Gleiche mit einem foreach:
<?php
$arrTest = range(1, 100000);
$intStart = microtime(true);
foreach ($arrTest as $intKey => $mixValue)
{
}
echo(microtime(true) - $intStart . " Sekunden\n");
Ergebnis:
0.0097959041595459 Sekunden
Im Endergebnis haben wir mit foreach eine wunderbare Zeit geschafft. Schauen wir uns das ganze einmal genauer an, so sehen wir jedoch, dass unser foreach doppelt so viel Zeit gebraucht hat, wie die zweite Variante aus unserem Test davor.
Aber warum?
Lassen wir unser Array mit foreach durchlaufen, werden verschiedene Aktionen ausgeführt. Das Ganze lässt sich am besten mit dem folgendem Code Snippet zeigen:
<?php
class ArrayTest implements Iterator
{
private $intPosition = 0;
private $arrData = array();
public $intRewindCounter = 0;
public $intCurrentCounter = 0;
public $intKeyCounter = 0;
public $intNextCounter = 0;
public $intValidCounter = 0;
public function __construct()
{
$this->intPosition = 0;
$this->arrData = range(1, 100000);
}
function rewind()
{
$this->intRewindCounter++;
$this->intPosition = 0;
}
function current()
{
$this->intCurrentCounter++;
return $this->arrData[$this->intPosition];
}
function key()
{
$this->intKeyCounter++;
return $this->intPosition;
}
function next()
{
$this->intNextCounter++;
++$this->intPosition;
}
function valid()
{
$this->intValidCounter++;
return isset($this->arrData[$this->intPosition]);
}
}
$objArrayTest = new ArrayTest();
$intStart = microtime(true);
foreach ($objArrayTest as $intKey => $mixValue)
{
}
echo(microtime(true) - $intStart . " Sekunden\n");
echo($objArrayTest->intRewindCounter . "x Rewind\n");
echo($objArrayTest->intCurrentCounter . "x Current\n");
echo($objArrayTest->intKeyCounter . "x Key\n");
echo($objArrayTest->intNextCounter . "x Next\n");
echo($objArrayTest->intValidCounter . "x Valid\n");
Ergebnis:
0.22693490982056 Sekunden 1x Rewind 100000x Current 100000x Key 100000x Next 100001x Valid
Wie wir sehen, werden bei jedem Durchlauf die folgenden vier Aktionen durchgeführt (in dieser Reihenfolge):
- Der Zeiger wird auf den nächsten (next) bzw. den ersten (rewind) Wert im Array gesetzt
- Es wird geprüft ob wir an einer gültigen Position sind (valid)
- Der Wert wird abgerufen (current)
- Der Schlüssel wird abgerufen (key)
Auch hier erscheint es logisch, das die Variante etwas langsamer ist, als das einfache hochzählen einer Zahl.
Warum ein foreach dennoch schneller ist, wie ein count() im Schleifenkopf, lässt sich wie folgt erklären: Die Logik vom foreach liegt im C-Code von PHP verankert und kann beim Durchlauf direkt auf die verschiedenen Speicherbereiche des Arrays zugreifen. Eine Möglichkeit die uns in PHP nicht zur Verfügung steht. Wir sind auf PHP Funktionen angewiesen, und die sind, in Zusammenarbeit mit unserem eigenen PHP Code, langsamer als im Kern verankerte Algorithmen.
Was ist die richtige Schleife?
Welche Schleife am Ende zum Einsatz kommt, muss jeder für sich entscheiden. Für alle drei Varianten gibt es Einsatzmöglichkeiten (auch für ein count() im Schleifenkopf). Will man ein Array von vorn bis hinten durchlaufen, sollte aber foreach die erste Wahl sein, denn schon bei einem assoziativen Array oder fehlenden Schlüsseln, hat man mit count() ein Problem.
Um foreach mit for zu vergleichen, sollte man in den for-Schleifen auch den Schlüßel und Wert des array abrufen. Die leeren Schleifen tun das genau nicht!.
foreach ist für das Durchlaufen eines array/objektes gedacht, nicht um Warteschleifen zu realisieren.
Danke für die Erklärung:)
So genau habe ich mir da noch nie Gedanken darüber gemacht, aber es ist auf jeden Fall auch interessant, den Hintergrund wieso etwas “so und so ist” zu erfahren:)
Ich habe es ja auch bei Ragtek schon geschrieben, komplett unsinnig solche Vergleiche. Diese Microoptimierung wird man niemals brauchen, daher einfach lesbaren Code schreiben, und das geht fast immer am besten mit foreach.
[...] den Kommentaren des ursprünglichen Artikels wurde zu Recht beanstandet, das der Vergleich zwischen for und foreach, so wie er dort beschrieben [...]