Wydajność FOR vs FOREACH w PHP

136

Po pierwsze, rozumiem, że w 90% aplikacji różnica w wydajności jest całkowicie nieistotna, ale muszę tylko wiedzieć, która konstrukcja jest szybsza. To i ...

Informacje dostępne obecnie w sieci są niejasne. Wiele osób twierdzi, że foreach jest złe, ale technicznie powinno być szybsze, ponieważ ma uprościć pisanie przechodzenia przez tablicę przy użyciu iteratorów. Iteratory, które znowu mają być szybsze, ale w PHP są również pozornie martwo wolne (czy to nie jest rzecz PHP?). Mówię o funkcjach tablicowych: next () prev () reset () itd. Cóż, jeśli są to nawet funkcje, a nie jedna z tych funkcji języka PHP, które wyglądają jak funkcje.

Aby trochę zawęzić : nie interesuje mnie przechodzenie po tablicach w krokach o wartości większej niż 1 (bez kroków ujemnych, tj. Odwrotna iteracja). Nie interesuje mnie również przechodzenie do i z dowolnych punktów, tylko od 0 do długości. Nie widzę również regularnego manipulowania tablicami z więcej niż 1000 kluczy, ale widzę, że tablica jest wielokrotnie przechodzona w logice aplikacji! Również jeśli chodzi o operacje, w dużej mierze tylko manipulacja strunami i echo.

Oto kilka witryn referencyjnych:
http://www.phpbench.com/
http://www.php.lt/benchmark/phpbench.php

Co słyszę wszędzie:

  • foreachjest wolny, a zatem for/ whilejest szybszy
  • PHP foreachkopiuje tablicę, po której iteruje; aby przyspieszyć, musisz użyć referencji
  • taki kod: jest szybszy niż plik$key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    foreach

Oto mój problem. Napisałem ten skrypt testowy: http://pastebin.com/1ZgK07US i bez względu na to, ile razy uruchamiam skrypt, otrzymuję coś takiego:

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

W skrócie:

  • foreachjest szybszy niż w foreachprzypadku odniesienia
  • foreach jest szybszy niż for
  • foreachjest szybszy niż forw przypadku tablicy mieszającej

Czy ktoś może wyjaśnić?

  1. czy robię coś źle?
  2. Czy odniesienia do PHP foreach naprawdę robią różnicę? Chodzi mi o to, dlaczego nie miałby go skopiować, jeśli przekazujesz przez odniesienie?
  3. Jaki jest równoważny kod iteratora dla instrukcji foreach; Widziałem kilka w sieci, ale za każdym razem, gdy je testuję, czas jest daleko; Przetestowałem również kilka prostych konstrukcji iteratorów, ale wydaje mi się, że nigdy nie uzyskałem nawet przyzwoitych wyników - czy iteratory tablic w PHP są po prostu okropne?
  4. Czy istnieją szybsze sposoby / metody / konstrukcje do iteracji przez tablicę inną niż FOR / FOREACH (i WHILE)?

Wersja PHP 5.3.0


Edycja: odpowiedź Z pomocą ludzi tutaj mogłem zebrać odpowiedzi na wszystkie pytania. Podsumuję je tutaj:

  1. "Czy robię coś źle?" Wydaje się, że konsensus jest następujący: tak, nie mogę używać echa w testach porównawczych. Osobiście nadal nie rozumiem, dlaczego echo jest jakąś funkcją z losowym czasem wykonania lub jak każda inna funkcja jest w jakiś sposób inna - to i zdolność tego skryptu do generowania dokładnie tych samych wyników dla każdego, lepiej niż wszystko, jest trudna wyjaśnić, chociaż po prostu „używasz echa” (cóż, czego powinienem był użyć). Jednak przyznaję, że test powinien być wykonany z czymś lepszym; chociaż nie przychodzi na myśl idealny kompromis.
  2. „Czy odniesienia do PHP foreach naprawdę robią różnicę? Mam na myśli, dlaczego nie miałoby ich kopiować, jeśli przekazujesz je przez referencję? ircmaxell pokazuje, że tak, dalsze testy wydają się dowodzić, że w większości przypadków odniesienia powinny być szybsze - chociaż biorąc pod uwagę mój powyższy fragment kodu, zdecydowanie nie oznacza to wszystkiego. Akceptuję, że problem jest prawdopodobnie zbyt nieintuicyjny, aby zawracać sobie głowę na takim poziomie i wymagałby czegoś ekstremalnego, takiego jak dekompilacja, aby faktycznie określić, która jest lepsza dla każdej sytuacji.
  3. „Jaki jest odpowiednik kodu iteratora dla instrukcji foreach? Widziałem kilka w sieci, ale za każdym razem, gdy je testuję, czas jest daleki; przetestowałem również kilka prostych konstrukcji iteratora, ale nigdy nie uzyskałem nawet przyzwoitych wyników - czy iteratory tablic w PHP są po prostu okropne? " ircmaxell udzielił odpowiedzi poniżej; chociaż kod może być ważny tylko dla wersji PHP> = 5
  4. „Czy istnieją szybsze sposoby / metody / konstrukcje do iteracji przez tablicę inną niż FOR / FOREACH (i WHILE)?” Dziękuję Gordonowi za odpowiedź. Używanie nowych typów danych w PHP5 powinno zapewnić albo wzrost wydajności, albo zwiększenie pamięci (z których każdy może być pożądany w zależności od sytuacji). Chociaż pod względem szybkości wiele nowych typów tablic nie wydaje się być lepszych niż array (), splpriorityqueue i splobjectstorage wydają się być znacznie szybsze. Link udostępniony przez Gordona: http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

Dziękuję wszystkim, którzy próbowali pomóc.

Prawdopodobnie będę trzymać się foreach (wersji bez odniesienia) dla każdego prostego przejścia.

srcspider
źródło
7
Zasada 2.71 benchmarkingu: nie odbijaj się od benchmarku.
Mchl
1
każdy z odniesieniem musi być oznaczony jako odniesienie. masz tam błędny wniosek. jakiekolwiek użycie odniesienia będzie oczywiście wolniejsze niż bez odniesienia, nawet w pętli do-while.
bcosca
2
Ponieważ dotyczy to php 5.3, możesz również rozważyć przetestowanie nowych typów danych Spl vs Arrays. Lub po prostu spójrz tutaj: matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
Gordon
@ Mchl: Uruchomiłem go kilka razy i otrzymałem te same wyniki - jeśli echo psuje test porównawczy, czy nie powinienem uzyskać całkowicie losowych wyników? chciałbym też coś powtórzyć i wypisać, żeby echo było dla mnie naprawdę ważne; jeśli foreach jest szybsze podczas echo, to jest to duży fragment kodu, w którym powinienem używać foreach. @ stillstanding: to, co słyszę, jest w zasadzie w stylu "odniesienie na każdym powoduje szybsze (zawsze), zawsze pisz z odniesieniem", dlatego tak testowałem - nie interesuje mnie porównanie z innymi pętlami odniesienia
srcspider,
2
te puste pytania powinny naturalnie zostać zakazane. a także zwodnicza witryna phpbench
Your Common Sense

Odpowiedzi:

111

Osobiście uważam, że wykorzystam to, co ma sens w kontekście. Osobiście prawie nigdy nie używam fordo przechodzenia przez tablicę. Używam go do innych typów iteracji, ale foreachjest po prostu zbyt łatwy ... W większości przypadków różnica czasu będzie minimalna.

Najważniejsze, na co należy zwrócić uwagę, to:

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

To droga pętla, ponieważ wywołuje licznik w każdej pojedynczej iteracji. Dopóki tego nie robisz, nie sądzę, że to naprawdę ma znaczenie ...

Jeśli chodzi o odniesienie, które robi różnicę, PHP używa kopiowania przy zapisie, więc jeśli nie piszesz do tablicy, będzie stosunkowo niewielki narzut podczas wykonywania pętli. Jeśli jednak zaczniesz modyfikować tablicę w tablicy, zaczniesz widzieć różnice między nimi (ponieważ trzeba będzie skopiować całą tablicę, a odniesienie może po prostu zmodyfikować w tekście) ...

Jeśli chodzi o iteratory, foreachjest równoważne:

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

    // Contents of loop in here

    $it->next();
}

O ile istnieją szybsze sposoby iteracji, tak naprawdę zależy to od problemu. Ale naprawdę muszę zapytać, dlaczego? Rozumiem, że chcesz zwiększyć wydajność, ale myślę, że tracisz czas na mikro-optymalizację. Pamiętaj, Premature Optimization Is The Root Of All Evil...

Edycja: na podstawie komentarza zdecydowałem się przeprowadzić szybki test porównawczy ...

$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";

A wyniki:

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

Więc jeśli modyfikujesz tablicę w pętli, użycie referencji jest kilka razy szybsze ...

A narzut dla samego odniesienia jest w rzeczywistości mniejszy niż kopiowanie tablicy (to jest w 5.3.2) ... Więc wydaje się (przynajmniej w 5.3.2), jakby referencje były znacznie szybsze ...

ircmaxell
źródło
1
Czy nie masz na myśli, że „[Nieplanowana] optymalizacja jest źródłem wszelkiego zła”? ;) Cóż, wszyscy robią to samo, więc nie jest to tak bardzo optymalizacja, jak jest: co jest „lepszym standardowym sposobem adaptacji”. Jeszcze kilka pytań bez odpowiedzi: Mówisz, ponieważ nie trzeba kopiować, ale czy użycie odniesienia również nie jest narzutem? Komentarz stillstanding w moim pytaniu wydaje się również nie zgadzać z Twoimi założeniami. Ponadto, dlaczego kod jest wolniejszy również w tym miejscu. Czy foreach zmieniło się w 5.3.0, aby przekonwertować dowolną tablicę () na obiekt (np. SplFixedArray)?
srcspider,
@srcspider: Edytowana odpowiedź z kodem testu porównawczego i wynikami pokazującymi odwołania są rzeczywiście znacznie szybsze niż bez odwołań ...
ircmaxell
1
"the better standard way to adopt." Wydajność @srcspider nie jest jedynym kryterium wyboru tego, co przyjąć. zwłaszcza w tak naciąganym przypadku. Szczerze mówiąc, marnujesz tylko swój czas
Twój zdrowy rozsądek
@Przełęcz. Shrapnel Zgadzam się w 100%. Czytelność i wydajność konserwacji atutem z dużym marginesem w tym konkretnym przypadku ... Zgadzam o wybranie standardu i wystaje z niego, ale podstawa to norma na innych czynników --more important-- ...
ircmaxell
@ircmaxell: szybkie uruchomienie skryptu wydaje się potwierdzać Twój punkt widzenia, ale chcę przyjrzeć się temu nieco dokładniej; Mogę edytować moje oryginalne pytanie, dodając więcej testów, aby uwzględnić niektóre z nowych funkcji 5.3. @Przełęcz. Shrapnel: FOR jest prawie uniwersalnym poziomem programowania w przedszkolu, FOREACH to prostsza składnia. Jeśli chodzi o czytelność, wydają się być na równi. To wszystko jest na tak niskim poziomie, że nie sądzę, aby konserwacja była problemem, jak w przypadku niektórych wzorców wysokiego poziomu. I nie sądzę, żebym marnował czas, ponieważ ta „podstawowa konstrukcja” odpowiadałaby za dużo kodu, który bym napisał. :)
srcspider
54

Nie jestem pewien, czy to takie zaskakujące. Większość ludzi, którzy kodują w PHP, nie jest dobrze zorientowana w tym, co PHP robi w rzeczywistości. Powiem kilka rzeczy, które będą prawdziwe przez większość czasu:

  1. Jeśli nie modyfikujesz zmiennej, wartość według wartości jest szybsza w PHP. Dzieje się tak, ponieważ i tak jest to liczone odniesienie, a przez wartość daje mniej do zrobienia. Wie, że w momencie, gdy modyfikujesz ten ZVAL (wewnętrzna struktura danych PHP dla większości typów), będzie musiał go zerwać w prosty sposób (skopiuj i zapomnij o innym ZVAL). Ale nigdy go nie modyfikujesz, więc to nie ma znaczenia. Odnośniki sprawiają, że jest to bardziej skomplikowane przy większej księgowości, którą trzeba zrobić, aby wiedzieć, co zrobić, gdy zmienisz zmienną. Więc jeśli jesteś tylko do odczytu, paradoksalnie lepiej nie podkreślać tego za pomocą &. Wiem, to sprzeczne z intuicją, ale to też prawda.

  2. Foreach nie jest powolne. A dla prostej iteracji warunek, dla którego testuje - „czy jestem na końcu tej tablicy” - jest wykonywany przy użyciu kodu natywnego, a nie instrukcji PHP. Nawet jeśli są to opkody buforowane przez APC, nadal jest wolniejsze niż kilka natywnych operacji wykonywanych na gołym metalu.

  3. Używanie pętli for "for ($ i = 0; $ i <count ($ x); $ i ++) jest powolne z powodu funkcji count () i braku zdolności PHP (lub właściwie dowolnego języka interpretowanego) do oceny podczas parsowania czy coś modyfikuje tablicę. Zapobiega to jednokrotnemu oszacowaniu liczby.

  4. Ale nawet jeśli naprawisz to za pomocą "$ c = count ($ x); for ($ i = 0; $ i <$ c; $ i ++) $ i <$ c jest w najlepszym razie zbiorem rozkazów Zend, tak jak jest $ i ++. W trakcie 100000 iteracji może to mieć znaczenie. Na poziomie natywnym każdy wie, co robić. Żadne rozkazy PHP nie są potrzebne do przetestowania warunku „czy jestem na końcu tej tablicy”.

  5. A co ze starą szkołą "while (list (" stuff? Cóż, użycie each (), current () itp.) Będzie wymagało co najmniej jednego wywołania funkcji, które nie jest wolne, ale nie darmowe. Tak, te to znowu kody operacyjne PHP! Więc chociaż + lista + każdy ma również swoje koszty.

Z tych powodów każdy jest, co zrozumiałe, najlepszą opcją dla prostej iteracji.

I nie zapominaj, że jest również najłatwiejszy do odczytania, więc jest korzystny dla wszystkich.

Jaimie Sirovich
źródło
To jest dokładnie wyjaśnienie, którego szukałem, dzięki.
hardsetting
Ta odpowiedź powinna naprawdę być uzupełnieniem lub podsumowaniem zaznaczonej odpowiedzi. Cieszę się, że to przeczytałem, dobra robota.
doz87
31

Jedną rzeczą, na którą należy zwrócić uwagę w testach porównawczych (zwłaszcza phpbench.com), jest to, że chociaż liczby są prawidłowe, testy nie. Wiele testów na phpbench.com robi rzeczy, które są trywialne i nadużywają zdolności PHP do buforowania wyszukiwań tablic w celu wypaczenia testów porównawczych lub w przypadku iteracji po tablicy nie testuje tego w rzeczywistych przypadkach (nikt nie pisze pustego pętle). Zrobiłem własne testy porównawcze, które, jak stwierdziłem, dość odzwierciedlają rzeczywiste wyniki i zawsze pokazują, że natywna iteracyjna składnia języka foreachwychodzi na wierzch (niespodzianka, niespodzianka).

//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
źródło
3

Jest rok 2020 i wiele rzeczy ewoluowało dzięki php 7.4 i opcache .

Oto test porównawczy OP ^, uruchomiony jako unix CLI , bez części echo i html.

Test został uruchomiony lokalnie na zwykłym komputerze.

php -v

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

Zmodyfikowany skrypt testowy:

<?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";

Wynik:

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

Jak widać, ewolucja jest szalona, ​​około 560 razy szybciej niż odnotowano w 2012 roku.

Na moich maszynach i serwerach, po licznych eksperymentach, podstawy pętli są najszybsze. Jest to jeszcze jaśniejsze w przypadku zagnieżdżonych pętli ( $ i $ j $ k ..)

Jest również najbardziej elastyczny w użyciu i moim zdaniem ma lepszą czytelność.

NVRM
źródło
0

Myślę, ale nie jestem pewien: forpętla przyjmuje dwie operacje do sprawdzania i zwiększania wartości. foreachładuje dane do pamięci, a następnie iteruje wszystkie wartości.

Eswer
źródło
7
Każdy ma swoją opinię, ludzie przychodzą do Stack Overflow, aby znaleźć odpowiedzi. Jeśli nie jesteś pewien, co podajesz, sprawdź kod źródłowy, dokumentację, poszukaj go w Google itp.
Sebastien F.
Ponieważ wydajność opiera się na badaniach i testach, powinieneś przedstawić pewne dowody. Prosimy o podanie odpowiednich referencji. Mam nadzieję, że poprawisz swoją odpowiedź.
Marwan Salim
Myślę, że zależy to również od rzeczywistego obciążenia serwera i tego, co chcesz robić w pętli. Myślę, że zależy to również od rzeczywistego obciążenia serwera i tego, co chcesz robić w pętli. Chciałem wiedzieć, czy iterując po tablicy numerowanej, powinienem lepiej użyć pętli foreach - lub pętli for, więc przeprowadziłem test porównawczy na sandbox.onlinephpfunctions.com z PHP 7.4. Wielokrotnie uruchamiam ten sam skrypt i każde uruchomienie daje inny wynik. Raz pętla for była szybsza, innym razem pętla foreach, a innym razem były równe.
Alexander Behling