Desempenho de FOR vs FOREACH em PHP

134

Antes de tudo, entendo que em 90% dos aplicativos a diferença de desempenho é completamente irrelevante, mas só preciso saber qual é a construção mais rápida. Isso e ...

As informações atualmente disponíveis na rede são confusas. Muitas pessoas dizem que foreach é ruim, mas tecnicamente deve ser mais rápido, pois é suposto simplificar a gravação de uma passagem de array usando iteradores. Iteradores, que novamente são mais rápidos, mas no PHP também são aparentemente muito lentos (ou isso não é algo do PHP?). Eu estou falando sobre as funções da matriz: next () prev () reset () etc., bem, se elas são funções pares e não um daqueles recursos da linguagem PHP que se parecem com funções.

Para restringir um pouco isso : não sou interessante em percorrer matrizes em etapas com nada mais que 1 (nenhuma etapa negativa também, isto é, iteração reversa). Também não estou interessado em uma travessia de e para pontos arbitrários, apenas 0 no comprimento. Também não vejo manipular matrizes com mais de 1000 chaves acontecendo regularmente, mas vejo uma matriz sendo percorrida várias vezes na lógica de um aplicativo! Também quanto às operações, em grande parte apenas manipulação e eco de cordas.

Aqui estão alguns sites de referência:
http://www.phpbench.com/
http://www.php.lt/benchmark/phpbench.php

O que eu ouço em todos os lugares:

  • foreaché lento e, portanto, for/ whileé mais rápido
  • PHPs foreachcopia a matriz que itera; para torná-lo mais rápido, você precisa usar referências
  • código como este: é mais rápido que um$key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    foreach

Aqui está o meu problema. Eu escrevi este script de teste: http://pastebin.com/1ZgK07US e não importa quantas vezes eu execute o script, recebo algo assim:

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

Em resumo:

  • foreaché mais rápido do que foreachcom referência
  • foreach é mais rápido que for
  • foreaché mais rápido do que forpara uma tabela de hash

Alguém pode explicar?

  1. Estou fazendo algo errado?
  2. A coisa de referência do PHP foreach está realmente fazendo a diferença? Quero dizer, por que não copiá-lo se você passar por referência?
  3. Qual é o código do iterador equivalente para a instrução foreach; Já vi alguns na rede, mas cada vez que os teste, o tempo está muito longe; Também testei algumas construções simples de iteradores, mas nunca obtive resultados decentes - os iteradores de matriz no PHP são terríveis?
  4. Existem maneiras / métodos / construções mais rápidos para iterar por uma matriz diferente de FOR / FOREACH (e WHILE)?

Versão do PHP 5.3.0


Edit: Answer Com a ajuda de pessoas daqui, pude reunir as respostas para todas as perguntas. Vou resumir aqui:

  1. "Estou fazendo algo errado?" O consenso parece ser: sim, não posso usar eco em benchmarks. Pessoalmente, ainda não vejo como o eco é alguma função com tempo aleatório de execução ou como qualquer outra função é de alguma forma diferente - isso e a capacidade desse script de gerar apenas os mesmos resultados exatos do foreach melhor do que tudo é difícil para explicar, embora apenas "você esteja usando eco" (bem, o que eu deveria estar usando). No entanto, admito que o teste deve ser feito com algo melhor; embora um compromisso ideal não venha à mente.
  2. "O PHP para cada coisa de referência está realmente fazendo a diferença? Quero dizer, por que não copiá-lo se você passar por referência?" O ircmaxell mostra que sim, mais testes parecem provar na maioria dos casos que a referência deve ser mais rápida - embora, dado o meu trecho de código acima, definitivamente não signifique tudo. Eu aceito que o problema provavelmente não seja intuitivo demais para se preocupar com esse nível e exigiria algo extremo, como descompilar, para realmente determinar qual é o melhor para cada situação.
  3. "Qual é o código do iterador equivalente para a instrução foreach; eu vi alguns na rede, mas cada vez que os testo, o tempo está muito distante; eu também testei algumas construções simples do iterador, mas nunca obtive resultados decentes - os iteradores de array no PHP são péssimos? " a ircmaxell forneceu a resposta abaixo; embora o código possa ser válido apenas para a versão PHP> = 5
  4. "Existem maneiras / métodos / construções mais rápidos para iterar através de uma matriz diferente de FOR / FOREACH (e WHILE)?" Agradecemos a Gordon pela resposta. O uso de novos tipos de dados no PHP5 deve oferecer um aumento no desempenho ou na memória (um dos quais pode ser desejável dependendo da sua situação). Embora em termos de velocidade muitos dos novos tipos de array não pareçam melhores que array (), a fila de prioridade e o armazenamento de objetos de paródia parecem ser substancialmente mais rápidos. Link fornecido por Gordon: http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

Obrigado a todos que tentaram ajudar.

Provavelmente vou me ater ao foreach (a versão sem referência) para qualquer passagem simples.

srcspider
fonte
7
Regra 2.71 do benchmarking: não faça eco ao benchmark.
Mchl
1
foreach com referência deve ser marcado contra com referência. você tem uma conclusão errada lá. obviamente, qualquer uso de uma referência será mais lento do que o de uma referência, mesmo em um loop do-while.
bcosca
2
Como este é para o php 5.3, você também pode considerar testar os novos tipos de dados spl vs matrizes. Ou apenas olhar aqui: matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
Gordon
@ Mchl: Eu executei algumas vezes e obtive os mesmos resultados - se o eco corrompe a referência, não devo obter resultados completamente aleatórios? também gostaria de iterar algo e produzi-lo para que o eco seja realmente muito importante para mim; se foreach for mais rápido ao ecoar, é um grande pedaço de código em que devo usar o foreach. @ notável: o que estou ouvindo é basicamente na linha de "referência no foreach torna mais rápido (sempre), sempre escreve com referência", é por isso que testei assim - não estou realmente interessado em comparação com outros loops de referência
Srcpider
2
essas perguntas vazias devem naturalmente ser banidas. bem como aquele site phpbench enganador
Seu senso comum

Respostas:

110

Minha opinião pessoal é usar o que faz sentido no contexto. Pessoalmente, eu quase nunca uso forpara atravessar array. Eu o uso para outros tipos de iteração, mas foreaché muito fácil ... A diferença de horário será mínima na maioria dos casos.

A grande coisa a observar é:

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

Esse é um loop caro, pois as chamadas contam com todas as iterações. Contanto que você não esteja fazendo isso, não acho que realmente importe ...

Quanto à referência que faz a diferença, o PHP usa a cópia na gravação; portanto, se você não gravar na matriz, haverá relativamente pouca sobrecarga durante o loop. No entanto, se você começar a modificar a matriz dentro da matriz, é aí que começará a ver diferenças entre elas (já que será necessário copiar toda a matriz e a referência pode apenas modificar inline) ...

Quanto aos iteradores, foreaché equivalente a:

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

    // Contents of loop in here

    $it->next();
}

Na medida em que existem maneiras mais rápidas de iterar, isso realmente depende do problema. Mas eu realmente preciso perguntar, por quê? Entendo que quero tornar as coisas mais eficientes, mas acho que você está desperdiçando seu tempo com uma micro-otimização. Lembre-se, Premature Optimization Is The Root Of All Evil...

Edit: Com base no comentário, eu decidi fazer uma corrida rápida de benchmark ...

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

E os resultados:

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

Portanto, se você estiver modificando a matriz no loop, é várias vezes mais rápido usar referências ...

E a sobrecarga apenas para a referência é realmente menor do que copiar a matriz (isso é na 5.3.2) ... Então, parece (na 5.3.2 pelo menos) como se as referências fossem significativamente mais rápidas ...

ircmaxell
fonte
1
Você não quer dizer "a otimização [não planejada] é a raiz de todo mal"? ;) Bom, todos fazem a mesma coisa, por isso não é tanto uma otimização como é: qual é "a melhor maneira padrão de adotar". Também mais algumas perguntas sem resposta: você diz que não precisa copiar, mas o uso da referência também não é uma sobrecarga? O comentário de stillstanding em minha pergunta também parece discordar de suas suposições. Além disso, por que o código está produzindo mais lentamente para referência lá também. O foreach mudou na 5.3.0 para converter qualquer array () em um objeto (por exemplo, SplFixedArray)?
Srcpider
@srcspider: resposta editado com código de referência e os resultados que mostram referências são realmente muito mais rápido do que os não-referências ...
ircmaxell
1
O "the better standard way to adopt." desempenho do @srcspider não é o único critério para escolher o que adotar. especialmente em um caso tão exagerado. Sinceramente, você está apenas perdendo seu tempo
Seu senso comum
@Col. Estilhaços Eu concordo 100%. Legibilidade e desempenho manutenção trunfo por uma grande margem, neste caso particular ... Estou de acordo sobre a escolha de um padrão e ficar com ele, mas de base que estandarte sobre outros fatores --mais importante-- ...
ircmaxell
@ircmaxell: executar rapidamente seu script parece provar seu ponto de vista, mas quero investigar um pouco mais; Posso editar minha pergunta original com mais testes para incluir alguns dos novos recursos da versão 5.3. @Col. Estilhaços: FOR é um nível quase universal do jardim de infância de programação, FOREACH é uma sintaxe mais simples. Quanto à legibilidade, eles parecem estar em pé de igualdade. Isso é tudo tão baixo nível que não acho que a manutenção seja um problema, como seria para alguns padrões de alto nível. E não acho que estou perdendo tempo, já que esse "construto básico" seria responsável por muito código que eu escreveria. :)
srcspider
54

Não tenho certeza se isso é tão surpreendente. A maioria das pessoas que codifica em PHP não é bem versada no que o PHP está realmente fazendo no bare metal. Vou declarar algumas coisas, que serão verdadeiras na maioria das vezes:

  1. Se você não está modificando a variável, o valor é mais rápido no PHP. Isso ocorre porque a referência é contada de qualquer maneira e o valor por valor diminui o seu desempenho. Ele sabe que, no momento em que você modifica o ZVAL (estrutura de dados interna do PHP para a maioria dos tipos), ele deve ser interrompido de maneira direta (copie-o e esqueça o outro ZVAL). Mas você nunca a modifica, por isso não importa. As referências tornam isso mais complicado com mais escrituração contábil para saber o que fazer quando você modifica a variável. Portanto, se você é somente leitura, paradoxalmente, é melhor não apontar isso com o &. Eu sei, é contra-intuitivo, mas também é verdade.

  2. Foreach não é lento. E para uma iteração simples, a condição em que está testando - "estou no final desta matriz" - é feita usando código nativo, não opcodes PHP. Mesmo que sejam opcodes em cache da APC, ainda é mais lento que um monte de operações nativas feitas no bare metal.

  3. O uso de um loop for "for ($ i = 0; $ i <count ($ x); $ i ++) é lento devido ao count () e à falta da capacidade do PHP (ou realmente de qualquer linguagem interpretada) avaliar em análise tempo, se alguma coisa modifica a matriz, o que impede a avaliação da contagem uma vez.

  4. Mas mesmo depois de corrigi-lo com "$ c = count ($ x); para ($ i = 0; $ i <$ c; $ i ++), o $ i <$ c é, na melhor das hipóteses, um monte de opcodes do Zend, como é o $ i ++. No curso de 100000 iterações, isso pode importar. O Foreach sabe no nível nativo o que fazer. Nenhum código de código PHP necessário para testar a condição "eu estou no final desta matriz".

  5. E a velha escola "while (list (" stuff? Bem, usando each (), current (), etc.), todos envolverão pelo menos uma chamada de função, que não é lenta, mas não é gratuita. Sim, essas são opcodes PHP novamente! Portanto, enquanto + list + cada um também tem seus custos.

Por esses motivos, foreach é compreensivelmente a melhor opção para iteração simples.

E não se esqueça, também é o mais fácil de ler, por isso é ganha-ganha.

Jaimie Sirovich
fonte
Esta é exatamente a explicação que eu estava procurando, obrigado.
hardsetting
Essa resposta deve realmente ser um complemento ou resumo da resposta marcada. Estou feliz por ler, bom trabalho.
Doz87
30

Uma coisa a ser observada nos benchmarks (especialmente o phpbench.com) é que, embora os números sejam bons, os testes não. Muitos dos testes realizados no phpbench.com são triviais e abusam da capacidade do PHP de armazenar em cache as pesquisas de array para distorcer os benchmarks ou, no caso de iterar sobre um array, na verdade, não é testado em casos reais (ninguém escreve vazio para rotações). Fiz meus próprios benchmarks que achei que refletem bastante os resultados do mundo real e sempre mostram a sintaxe iterativa nativa do idioma foreachsaindo por cima (surpresa, surpresa).

//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
fonte
3

É 2020 e os materiais evoluíram bastante com o php 7.4 e o opcache .

Aqui está o benchmark OP ^, executado como CLI unix , sem as partes echo e html.

O teste foi executado localmente em um computador comum.

php -v

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

Script de benchmark modificado:

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

Resultado:

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

Como você pode ver, a evolução é insana, cerca de 560 vezes mais rápida do que a relatada em 2012.

Nas minhas máquinas e servidores, após minhas inúmeras experiências, o básico para loops é o mais rápido. Isso é ainda mais claro usando loops aninhados ( $ i $ j $ k ..)

É também o mais flexível no uso e tem uma melhor legibilidade do meu ponto de vista.

NVRM
fonte
0

Penso, mas não tenho certeza: o forloop leva duas operações para verificar e incrementar valores. foreachcarrega os dados na memória e itera todos os valores.

Eswer
fonte
7
Todo mundo tem uma opinião, as pessoas vêm ao Stack Overflow para encontrar respostas. Se você não tiver certeza do que está declarando, verifique o código-fonte, a documentação, faça uma pesquisa no google etc.
Sebastien F. -
Como o desempenho é baseado em pesquisas e testes, você deve apresentar algumas evidências. Forneça suas referências de acordo. Espero que você possa melhorar sua resposta.
Marwan Salim
Eu acho que isso também depende da carga real do servidor e do que você deseja fazer no loop. Eu acho que isso também depende da carga real do servidor e do que você deseja fazer no loop. Eu queria saber se, ao iterar sobre a matriz numerada, devo usar melhor um foreach - ou for-loop, então executei uma referência no sandbox.onlinephpfunctions.com com o PHP 7.4. Eu executo repetidamente os mesmos tempos de multiplicação de scripts e cada execução me dá um resultado diferente. Uma vez o loop for foi mais rápido, outra vez o loop foreach e outra vez foram iguais.
Alexander Behling