Leistung von FOR vs FOREACH in PHP

134

Zunächst einmal verstehe ich, dass in 90% der Anwendungen der Leistungsunterschied völlig irrelevant ist, aber ich muss nur wissen, welches das schnellere Konstrukt ist. Das und ...

Die Informationen, die derzeit im Internet verfügbar sind, sind verwirrend. Viele Leute sagen, dass foreach schlecht ist, aber technisch sollte es schneller sein, da es das Schreiben eines Array-Traversals mit Iteratoren vereinfachen soll. Iteratoren, von denen wiederum angenommen wird, dass sie schneller sind, aber in PHP anscheinend auch absolut langsam sind (oder ist dies keine PHP-Sache?). Ich spreche von den Array-Funktionen: next () prev () reset () usw. Nun, wenn es sich um gerade Funktionen handelt und nicht um eine dieser PHP-Sprachfunktionen, die wie Funktionen aussehen.

Um dies ein wenig einzugrenzen : Ich bin nicht daran interessiert, Arrays in Schritten von mehr als 1 zu durchlaufen (auch keine negativen Schritte, dh umgekehrte Iteration). Ich bin auch nicht an einer Durchquerung von und zu beliebigen Punkten interessiert, nur 0 bis zur Länge. Ich sehe auch nicht, dass Arrays mit mehr als 1000 Schlüsseln regelmäßig manipuliert werden, aber ich sehe, dass ein Array in der Logik einer Anwendung mehrmals durchlaufen wird! Auch für Operationen, größtenteils nur String-Manipulation und Echo.

Hier einige Referenzseiten:
http://www.phpbench.com/
http://www.php.lt/benchmark/phpbench.php

Was ich überall höre:

  • foreachist langsam und somit for/ whileist schneller
  • PHPs kopieren foreachdas Array, über das es iteriert. Um es schneller zu machen, müssen Sie Referenzen verwenden
  • Code wie folgt : ist schneller als ein$key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    foreach

Hier ist mein Problem. Ich habe dieses Testskript geschrieben: http://pastebin.com/1ZgK07US und egal wie oft ich das Skript ausführe, ich erhalte Folgendes:

foreach 1.1438131332397
foreach (using reference) 1.2919359207153
for 1.4262869358063
foreach (hash table) 1.5696921348572
for (hash table) 2.4778981208801

Zusamenfassend:

  • foreachist schneller als foreachmit Referenz
  • foreach ist schneller als for
  • foreachist schneller als forfür eine Hash-Tabelle

Kann jemand erklären?

  1. Mache ich etwas falsch?
  2. Macht PHP für jede Referenz wirklich einen Unterschied? Ich meine, warum sollte es nicht kopiert werden, wenn Sie als Referenz übergeben werden?
  3. Was ist der äquivalente Iteratorcode für die foreach-Anweisung? Ich habe einige im Netz gesehen, aber jedes Mal, wenn ich sie teste, ist das Timing weit entfernt. Ich habe auch ein paar einfache Iteratorkonstrukte getestet, aber nie anständige Ergebnisse zu erzielen - sind die Array-Iteratoren in PHP einfach schrecklich?
  4. Gibt es schnellere Möglichkeiten / Methoden / Konstrukte, um durch ein anderes Array als FOR / FOREACH (und WHILE) zu iterieren?

PHP Version 5.3.0


Bearbeiten: Antwort Mit Hilfe der Leute hier konnte ich die Antworten auf alle Fragen zusammensetzen. Ich werde sie hier zusammenfassen:

  1. "Mache ich etwas falsch?" Der Konsens scheint zu sein: Ja, ich kann Echo nicht in Benchmarks verwenden. Persönlich sehe ich immer noch nicht, wie Echo eine Funktion mit zufälliger Ausführungszeit ist oder wie eine andere Funktion irgendwie anders ist - das und die Fähigkeit dieses Skripts, genau die gleichen Ergebnisse von foreach besser als alles zu generieren, ist schwierig zu erklären, obwohl nur "Sie verwenden Echo" (nun, was hätte ich verwenden sollen). Ich gebe jedoch zu, dass der Test mit etwas Besserem durchgeführt werden sollte; Ein idealer Kompromiss fällt mir jedoch nicht ein.
  2. "Macht PHP für jede Referenzsache wirklich einen Unterschied? Ich meine, warum sollte es nicht kopiert werden, wenn Sie als Referenz übergeben werden?" ircmaxell zeigt, dass dies der Fall ist. Weitere Tests scheinen zu beweisen, dass die Referenz in den meisten Fällen schneller sein sollte - obwohl angesichts meines obigen Codeausschnitts definitiv nicht alles bedeutet. Ich akzeptiere, dass das Problem wahrscheinlich zu nicht intuitiv ist, um es auf einer solchen Ebene zu behandeln, und dass etwas Extremes wie das Dekompilieren erforderlich wäre, um tatsächlich zu bestimmen, welches für jede Situation besser ist.
  3. "Was ist der äquivalente Iterator-Code für die foreach-Anweisung? Ich habe einige im Internet gesehen, aber jedes Mal, wenn ich sie teste, ist das Timing weit entfernt. Ich habe auch einige einfache Iterator-Konstrukte getestet, aber nie anständige Ergebnisse zu erzielen - Sind die Array-Iteratoren in PHP einfach schrecklich? " ircmaxell lieferte die Antwort unten; Der Code ist jedoch möglicherweise nur für die PHP-Version> = 5 gültig
  4. "Gibt es schnellere Möglichkeiten / Methoden / Konstrukte, um durch ein anderes Array als FOR / FOREACH (und WHILE) zu iterieren?" Danke an Gordon für die Antwort. Die Verwendung neuer Datentypen in PHP5 sollte entweder eine Leistungssteigerung oder eine Speichersteigerung bewirken (je nach Ihrer Situation kann dies wünschenswert sein). Während die Geschwindigkeit vieler neuer Array-Typen nicht besser zu sein scheint als array (), scheinen die Splpriorityqueue und der Splobject-Speicher wesentlich schneller zu sein. Link bereitgestellt von Gordon: http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

Vielen Dank an alle, die versucht haben zu helfen.

Ich werde mich wahrscheinlich für jede einfache Durchquerung an foreach (die Nicht-Referenzversion) halten.

srcspider
quelle
7
Regel 2.71 des Benchmarking: Echo nicht zum Benchmarking.
Mchl
1
foreach with reference muss mit for referenziert werden. Sie haben dort eine fehlerhafte Schlussfolgerung. Jede Verwendung einer Referenz wird offensichtlich langsamer sein als die ohne Referenz, selbst in einer Do-While-Schleife.
Bcosca
2
Da dies für PHP 5.3 ist, sollten Sie auch die neuen Spl-Datentypen gegen Arrays testen. Oder schauen Sie einfach hier: matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
Gordon
@ Mchl: Ich habe es ein paar Mal ausgeführt und die gleichen Ergebnisse erzielt. Wenn das Echo den Benchmark beschädigt, sollte ich dann keine völlig zufälligen Ergebnisse erzielen? Außerdem möchte ich etwas iterieren und ausgeben, damit das Echo für mich wirklich wichtig ist. Wenn foreach beim Echo schneller ist, dann ist das ein großer Teil des Codes, in dem ich foreach verwenden sollte. @ stillstanding: Was ich höre, ist im Grunde genommen im Sinne von "Referenz in foreach macht schneller (immer), immer mit Referenz schreiben", deshalb habe ich so getestet - ich bin nicht wirklich an einem Vergleich mit anderen Referenzschleifen interessiert
srcspider
2
Diese leeren Fragen sollten natürlich verboten werden. sowie diese täuschende phpbench Seite
Ihr

Antworten:

110

Meine persönliche Meinung ist es, das zu verwenden, was im Kontext Sinn macht. Persönlich verwende ich fast nie forfür Array-Traversal. Ich benutze es für andere Arten der Iteration, aber es foreachist einfach zu einfach ... Der Zeitunterschied wird in den meisten Fällen minimal sein.

Das Wichtigste ist:

for ($i = 0; $i < count($array); $i++) {

Dies ist eine teure Schleife, da Aufrufe bei jeder einzelnen Iteration zählen. Solange du das nicht tust, denke ich nicht, dass es wirklich wichtig ist ...

Was die Referenz betrifft, die einen Unterschied macht, verwendet PHP Copy-on-Write. Wenn Sie also nicht in das Array schreiben, entsteht beim Schleifen relativ wenig Overhead. Wenn Sie jedoch anfangen, das Array innerhalb des Arrays zu ändern, werden Sie dort Unterschiede zwischen ihnen feststellen (da das gesamte Array kopiert werden muss und die Referenz nur inline geändert werden kann) ...

Was die Iteratoren betrifft, foreachentspricht dies:

$it->rewind();
while ($it->valid()) {
    $key = $it->key();     // If using the $key => $value syntax
    $value = $it->current();

    // Contents of loop in here

    $it->next();
}

Soweit es schnellere Möglichkeiten zum Iterieren gibt, hängt es wirklich vom Problem ab. Aber ich muss wirklich fragen, warum? Ich verstehe, dass Sie die Dinge effizienter gestalten wollen, aber ich denke, Sie verschwenden Ihre Zeit für eine Mikrooptimierung. Denken Sie daran, Premature Optimization Is The Root Of All Evil...

Bearbeiten: Aufgrund des Kommentars habe ich mich für einen schnellen Benchmark-Lauf entschieden ...

$a = array();
for ($i = 0; $i < 10000; $i++) {
    $a[] = $i;
}

$start = microtime(true);
foreach ($a as $k => $v) {
    $a[$k] = $v + 1;
}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => &$v) {
    $v = $v + 1;
}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => $v) {}
echo "Completed in ", microtime(true) - $start, " Seconds\n";

$start = microtime(true);
foreach ($a as $k => &$v) {}    
echo "Completed in ", microtime(true) - $start, " Seconds\n";

Und die Ergebnisse:

Completed in 0.0073502063751221 Seconds
Completed in 0.0019769668579102 Seconds
Completed in 0.0011849403381348 Seconds
Completed in 0.00111985206604 Seconds

Wenn Sie also das Array in der Schleife ändern, ist die Verwendung von Referenzen um ein Vielfaches schneller ...

Und der Overhead für nur die Referenz ist tatsächlich geringer als das Kopieren des Arrays (dies ist in 5.3.2) ... Es scheint also (zumindest in 5.3.2), als ob Referenzen wesentlich schneller sind ...

ircmaxell
quelle
1
Meinst du nicht "[Nicht geplante] Optimierung ist die Wurzel allen Übels"? ;) Nun, alle machen das Gleiche, es ist also weniger eine Optimierung als vielmehr eine "bessere Standardmethode". Auch noch ein paar Fragen unbeantwortet: Sie sagen, weil es nicht kopiert werden muss, aber ist die Verwendung von Referenz nicht auch ein Overhead? Der Kommentar von stillstanding in meiner Frage scheint auch Ihren Annahmen nicht zu entsprechen. Warum produziert der Code dort auch langsamer als Referenz? Hat sich das foreach in 5.3.0 geändert, um ein Array () in ein Objekt (z. B. SplFixedArray) zu konvertieren?
srcspider
@srcspider: Bearbeitete Antwort mit Benchmark-Code und Ergebnisse, die Referenzen zeigen, sind in der Tat viel schneller als Nicht-Referenzen ...
ircmaxell
1
Die "the better standard way to adopt." Leistung von @srcspider ist nicht das einzige Kriterium für die Auswahl der zu verwendenden Elemente . besonders in solch einem weit hergeholten Fall. Ehrlich gesagt, verschwenden Sie nur Ihre Zeit
Ihr gesunder Menschenverstand
@Col. Schrapnell Ich stimme 100% zu. Lesbarkeit und Wartbarkeit übertreffen die Leistung in diesem speziellen Fall bei weitem ... Ich stimme zu, einen Standard auszuwählen und daran festzuhalten, aber stütze diesen Standard auf andere - wichtigere - Faktoren ...
ircmaxell
@ircmaxell: Das schnelle Ausführen Ihres Skripts scheint Ihren Standpunkt zu beweisen, aber ich möchte es etwas genauer untersuchen. Ich könnte meine ursprüngliche Frage mit mehr Tests bearbeiten, einschließlich einiger der neuen 5.3-Funktionen. @Col. Shrapnel: FOR ist eine fast universelle Programmierstufe, FOREACH ist eine einfachere Syntax. In Bezug auf die Lesbarkeit scheinen sie gleichberechtigt zu sein. Dies ist alles so niedrig, dass ich nicht denke, dass Wartung ein Problem ist, wie es für ein Muster auf hoher Ebene der Fall wäre. Und ich glaube nicht, dass ich meine Zeit verschwenden werde, da dieses "Grundkonstrukt" viel Code ausmachen würde, den ich schreiben würde. :)
srcspider
54

Ich bin mir nicht sicher, ob das so überraschend ist. Die meisten Leute, die in PHP programmieren, sind nicht gut darin versiert, was PHP tatsächlich auf dem Bare Metal macht. Ich werde ein paar Dinge sagen, die die meiste Zeit wahr sein werden:

  1. Wenn Sie die Variable nicht ändern, ist der By-Value in PHP schneller. Dies liegt daran, dass die Referenz ohnehin gezählt wird und der Nachwert weniger zu tun gibt. Es weiß, dass ZVAL (die interne Datenstruktur von PHP für die meisten Typen) in der Sekunde, in der Sie es ändern, auf einfache Weise abgebrochen werden muss (kopieren Sie es und vergessen Sie das andere ZVAL). Aber Sie ändern es nie, also spielt es keine Rolle. Referenzen machen dies komplizierter, da mehr Buchhaltung erforderlich ist, um zu wissen, was zu tun ist, wenn Sie die Variable ändern. Wenn Sie also schreibgeschützt sind, ist es paradoxerweise besser, nicht auf das & hinzuweisen. Ich weiß, es ist nicht intuitiv, aber es ist auch wahr.

  2. Foreach ist nicht langsam. Und für eine einfache Iteration wird die Bedingung, gegen die getestet wird - "Bin ich am Ende dieses Arrays" - mit nativem Code und nicht mit PHP-Opcodes ausgeführt. Selbst wenn es sich um APC-zwischengespeicherte Opcodes handelt, ist es immer noch langsamer als eine Reihe nativer Operationen, die am Bare Metal ausgeführt werden.

  3. Die Verwendung einer for-Schleife "für ($ i = 0; $ i <count ($ x); $ i ++) ist aufgrund der count () und der mangelnden Fähigkeit von PHP (oder einer wirklich interpretierten Sprache), bei der Analyse auszuwerten, langsam Zeit, ob irgendetwas das Array ändert. Dies verhindert, dass es die Anzahl einmal auswertet.

  4. Aber selbst wenn Sie es einmal mit "$ c = count ($ x); für ($ i = 0; $ i <$ c; $ i ++) reparieren, ist $ i <$ c bestenfalls eine Reihe von Zend-Opcodes Das $ i ++. Im Verlauf von 100000 Iterationen kann dies von Bedeutung sein. Foreach weiß auf nativer Ebene, was zu tun ist. Es sind keine PHP-Opcodes erforderlich, um die Bedingung "Bin ich am Ende dieses Arrays" zu testen.

  5. Was ist mit der alten Schule? "While (list (" stuff? Nun, die Verwendung von each (), current () usw. wird mindestens einen Funktionsaufruf beinhalten, der nicht langsam, aber nicht kostenlos ist. Ja, diese sind wieder PHP-Opcodes! Also während + Liste + hat jeder auch seine Kosten.

Aus diesen Gründen ist foreach verständlicherweise die beste Option für eine einfache Iteration.

Und vergessen Sie nicht, es ist auch am einfachsten zu lesen, also Win-Win.

Jaimie Sirovich
quelle
Dies ist genau die Erklärung, nach der ich gesucht habe, danke.
Hardsetting
Diese Antwort sollte wirklich eine Ergänzung oder Zusammenfassung der markierten Antwort sein. Ich bin froh, dass ich es gelesen habe, gute Arbeit.
doz87
30

Eine Sache, auf die Sie bei Benchmarks (insbesondere bei phpbench.com) achten sollten, ist, dass die Tests zwar solide sind, die Tests jedoch nicht. Viele der Tests auf phpbench.com sind trivial und missbrauchen die Fähigkeit von PHP, Array-Lookups zwischenzuspeichern, um Benchmarks zu verzerren, oder im Falle des Iterierens über ein Array dies in realen Fällen nicht zu testen (niemand schreibt leer für Schleifen). Ich habe meine eigenen Benchmarks erstellt, die die Ergebnisse der realen Welt widerspiegeln und immer die native iterierende Syntax der Sprache zeigen foreach(Überraschung, Überraschung).

//make a nicely random array
$aHash1 = range( 0, 999999 );
$aHash2 = range( 0, 999999 );
shuffle( $aHash1 );
shuffle( $aHash2 );
$aHash = array_combine( $aHash1, $aHash2 );


$start1 = microtime(true);
foreach($aHash as $key=>$val) $aHash[$key]++;
$end1 = microtime(true);

$start2 = microtime(true);
while(list($key) = each($aHash)) $aHash[$key]++;
$end2 = microtime(true);


$start3 = microtime(true);
$key = array_keys($aHash);
$size = sizeOf($key);
for ($i=0; $i<$size; $i++) $aHash[$key[$i]]++;
$end3 = microtime(true);

$start4 = microtime(true);
foreach($aHash as &$val) $val++;
$end4 = microtime(true);

echo "foreach ".($end1 - $start1)."\n"; //foreach 0.947947025299
echo "while ".($end2 - $start2)."\n"; //while 0.847212076187
echo "for ".($end3 - $start3)."\n"; //for 0.439476966858
echo "foreach ref ".($end4 - $start4)."\n"; //foreach ref 0.0886030197144

//For these tests we MUST do an array lookup,
//since that is normally the *point* of iteration
//i'm also calling noop on it so that PHP doesn't
//optimize out the loopup.
function noop( $value ) {}

//Create an array of increasing indexes, w/ random values
$bHash = range( 0, 999999 );
shuffle( $bHash );

$bstart1 = microtime(true);
for($i = 0; $i < 1000000; ++$i) noop( $bHash[$i] );
$bend1 = microtime(true);

$bstart2 = microtime(true);
$i = 0; while($i < 1000000) { noop( $bHash[$i] ); ++$i; }
$bend2 = microtime(true);


$bstart3 = microtime(true);
foreach( $bHash as $value ) { noop( $value ); }
$bend3 = microtime(true);

echo "for ".($bend1 - $bstart1)."\n"; //for 0.397135972977
echo "while ".($bend2 - $bstart2)."\n"; //while 0.364789962769
echo "foreach ".($bend3 - $bstart3)."\n"; //foreach 0.346374034882
Kendall Hopkins
quelle
3

Es ist 2020 und die Dinge haben sich mit PHP 7.4 und Opcache stark weiterentwickelt .

Hier ist der OP ^ -Benchmark, der als Unix- CLI ohne die Teile echo und html ausgeführt wird.

Der Test wurde lokal auf einem normalen Computer ausgeführt.

php -v

PHP 7.4.6 (cli) (built: May 14 2020 10:02:44) ( NTS )

Modifiziertes Benchmark-Skript:

<?php 
 ## preperations; just a simple environment state

  $test_iterations = 100;
  $test_arr_size = 1000;

  // a shared function that makes use of the loop; this should
  // ensure no funny business is happening to fool the test
  function test($input)
  {
    //echo '<!-- '.trim($input).' -->';
  }

  // for each test we create a array this should avoid any of the
  // arrays internal representation or optimizations from getting
  // in the way.

  // normal array
  $test_arr1 = array();
  $test_arr2 = array();
  $test_arr3 = array();
  // hash tables
  $test_arr4 = array();
  $test_arr5 = array();

  for ($i = 0; $i < $test_arr_size; ++$i)
  {
    mt_srand();
    $hash = md5(mt_rand());
    $key = substr($hash, 0, 5).$i;

    $test_arr1[$i] = $test_arr2[$i] = $test_arr3[$i] = $test_arr4[$key] = $test_arr5[$key]
      = $hash;
  }

  ## foreach

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr1 as $k => $v)
    {
      test($v);
    }
  }
  echo 'foreach '.(microtime(true) - $start)."\n";  

  ## foreach (using reference)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr2 as &$value)
    {
      test($value);
    }
  }
  echo 'foreach (using reference) '.(microtime(true) - $start)."\n";

  ## for

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    $size = count($test_arr3);
    for ($i = 0; $i < $size; ++$i)
    {
      test($test_arr3[$i]);
    }
  }
  echo 'for '.(microtime(true) - $start)."\n";  

  ## foreach (hash table)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    foreach ($test_arr4 as $k => $v)
    {
      test($v);
    }
  }
  echo 'foreach (hash table) '.(microtime(true) - $start)."\n";

  ## for (hash table)

  $start = microtime(true);
  for ($j = 0; $j < $test_iterations; ++$j)
  {
    $keys = array_keys($test_arr5);
    $size = sizeOf($test_arr5);
    for ($i = 0; $i < $size; ++$i)
    {
      test($test_arr5[$keys[$i]]);
    }
  }
  echo 'for (hash table) '.(microtime(true) - $start)."\n";

Ausgabe:

foreach 0.0032877922058105
foreach (using reference) 0.0029420852661133
for 0.0025191307067871
foreach (hash table) 0.0035080909729004
for (hash table) 0.0061779022216797

Wie Sie sehen können, ist die Entwicklung verrückt, ungefähr 560 Mal schneller als 2012 berichtet.

Auf meinen Maschinen und Servern sind nach meinen zahlreichen Experimenten die Grundlagen für Schleifen am schnellsten. Dies ist mit verschachtelten Schleifen ( $ i $ j $ k ..) noch deutlicher .

Es ist auch am flexibelsten in der Verwendung und hat aus meiner Sicht eine bessere Lesbarkeit.

NVRM
quelle
0

Ich denke, aber ich bin nicht sicher: Die forSchleife benötigt zwei Operationen zum Überprüfen und Inkrementieren von Werten. foreachLädt die Daten in den Speicher, dann werden alle Werte wiederholt.

Eswer
quelle
7
Jeder hat eine Meinung, die Leute kommen zu Stack Overflow, um Antworten zu finden. Wenn Sie sich nicht sicher sind, was Sie angeben, überprüfen Sie den Quellcode, die Dokumentation, führen Sie eine Google-Suche usw. durch
Sebastien F.
Da die Leistung auf Forschung und Tests basiert, sollten Sie einige Beweise vorlegen. Bitte geben Sie Ihre Referenzen entsprechend an. Hoffentlich können Sie Ihre Antwort verbessern.
Marwan Salim
Ich denke, es hängt auch von der tatsächlichen Auslastung des Servers ab und davon, was Sie in der Schleife tun möchten. Ich denke, es hängt auch von der tatsächlichen Auslastung des Servers ab und davon, was Sie in der Schleife tun möchten. Ich wollte wissen, ob ich beim Iterieren über ein nummeriertes Array besser eine foreach- oder for-Schleife verwenden sollte, also habe ich mit PHP 7.4 einen Benchmark auf sandbox.onlinephpfunctions.com durchgeführt . Ich führe das gleiche Skript mehrmals wiederholt aus und jeder Lauf führt zu einem anderen Ergebnis. Einmal war die for-Schleife schneller, ein anderes Mal war die foreach-Schleife schneller und ein anderes Mal waren sie gleich.
Alexander Behling