Rendimiento de FOR vs FOREACH en PHP

136

En primer lugar, entiendo que en el 90% de las aplicaciones la diferencia de rendimiento es completamente irrelevante, pero solo necesito saber cuál es la construcción más rápida. Eso y ...

La información actualmente disponible sobre ellos en la red es confusa. Mucha gente dice que foreach es malo, pero técnicamente debería ser más rápido, ya que se supone que simplifica la escritura de un recorrido de matriz usando iteradores. Los iteradores, que nuevamente se supone que son más rápidos, pero en PHP también aparentemente son muy lentos (¿o no es esto una cosa de PHP?). Estoy hablando de las funciones de matriz: next () prev () reset () etc. bueno, si son incluso funciones y no una de esas características del lenguaje PHP que parecen funciones.

Para reducir esto un poco : no me interesa atravesar matrices en pasos de más de 1 (tampoco hay pasos negativos, es decir, iteración inversa). Tampoco estoy interesado en un recorrido hacia y desde puntos arbitrarios, solo 0 a la longitud. Tampoco veo la manipulación de matrices con más de 1000 claves sucediendo de forma regular, ¡pero sí veo una matriz atravesada varias veces en la lógica de una aplicación! También en cuanto a las operaciones, en gran parte solo la manipulación de cuerdas y el eco.

Aquí hay algunos sitios de referencia:
http://www.phpbench.com/
http://www.php.lt/benchmark/phpbench.php

Lo que escucho en todas partes:

  • foreaches lento, y por lo tanto for/ whilees más rápido
  • PHP foreachcopia la matriz sobre la que itera; para hacerlo más rápido necesitas usar referencias
  • código como este: es más rápido que un$key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    foreach

Este es mi problema. Escribí este script de prueba: http://pastebin.com/1ZgK07US y no importa cuántas veces ejecute el script, obtengo algo como esto:

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

En breve:

  • foreaches más rápido que foreachcon referencia
  • foreach es más rápido que for
  • foreaches más rápido que forpara una tabla hash

¿Alguien puede explicarlo?

  1. ¿Estoy haciendo algo mal?
  2. ¿PHP para cada cosa de referencia realmente está haciendo una diferencia? Quiero decir, ¿por qué no lo copiaría si pasa por referencia?
  3. ¿Cuál es el código de iterador equivalente para la instrucción foreach? He visto algunos en la red, pero cada vez que los pruebo, el tiempo está muy lejos; También probé algunas construcciones de iteradores simples, pero parece que nunca obtengo resultados decentes: ¿los iteradores de matriz en PHP son simplemente horribles?
  4. ¿Hay formas / métodos / construcciones más rápidas para iterar a través de una matriz que no sea FOR / FOREACH (y WHILE)?

Versión de PHP 5.3.0


Editar: Respuesta Con la ayuda de la gente de aquí, pude juntar las respuestas a todas las preguntas. Los resumiré aquí:

  1. "¿Estoy haciendo algo mal?" El consenso parece ser: sí, no puedo usar el eco en los puntos de referencia. Personalmente, todavía no veo cómo echo es alguna función con tiempo aleatorio de ejecución o cómo cualquier otra función es de alguna manera diferente; eso y la capacidad de ese script para generar exactamente los mismos resultados de foreach mejor que todo es difícil para explicar aunque solo "estás usando echo" (bueno, ¿qué debería haber estado usando). Sin embargo, reconozco que la prueba debería hacerse con algo mejor; aunque no se me ocurre un compromiso ideal.
  2. "¿PHP para cada cosa de referencia realmente hace una diferencia? Quiero decir, ¿por qué no lo copiaría si pasa por referencia? ircmaxell muestra que sí, las pruebas adicionales parecen demostrar que en la mayoría de los casos la referencia debería ser más rápida, aunque dado mi fragmento de código anterior, definitivamente no significa todo. Acepto que el problema probablemente no sea demasiado intuitivo para molestarse en ese nivel y requeriría algo extremo, como la descompilación, para determinar cuál es mejor para cada situación.
  3. "¿Cuál es el código de iterador equivalente para la declaración foreach? He visto algunos en la red, pero cada vez que los pruebo, el tiempo está muy lejos. También probé algunas construcciones de iterador simples pero nunca parece obtener resultados decentes - ¿Son los iteradores de matriz en PHP simplemente horribles? " ircmaxell proporcionó la respuesta a continuación; aunque el código solo puede ser válido para la versión PHP> = 5
  4. "¿Hay formas / métodos / construcciones más rápidas para iterar a través de una matriz que no sea FOR / FOREACH (y WHILE)?" Gracias a Gordon por la respuesta. El uso de nuevos tipos de datos en PHP5 debería mejorar el rendimiento o la memoria (cualquiera de los cuales podría ser deseable según su situación). Si bien en cuanto a la velocidad, muchos de los nuevos tipos de arreglos no parecen ser mejores que arreglo (), splpriorityqueue y splobjectstorage parecen ser sustancialmente más rápidos. Enlace proporcionado por Gordon: http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

Gracias a todos los que intentaron ayudar.

Probablemente me quedo con foreach (la versión sin referencia) para cualquier recorrido simple.

srcspider
fuente
7
Regla 2.71 de la evaluación comparativa: no se haga eco de la evaluación comparativa.
Mchl
1
foreach con referencia debe estar marcado contra para con referencia. tienes una conclusión errónea allí. cualquier uso de una referencia obviamente será más lento que sin una referencia, incluso en un ciclo do-while.
bcosca
2
Dado que esto es para php 5.3, es posible que también desee considerar probar los nuevos tipos de datos Spl vs Arrays. O simplemente mire aquí: matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
Gordon
@ Mchl: Lo ejecuté varias veces y obtuve los mismos resultados; si el eco corrompe el punto de referencia, ¿no debería obtener resultados completamente aleatorios? también me gustaría iterar algo y generarlo para que el eco sea realmente importante para mí; si foreach es más rápido al hacer eco, entonces es una gran parte del código donde debería usar foreach. @ still Standing: lo que estoy escuchando es básicamente en la línea de "la referencia en foreach hace más rápido (siempre), siempre escribe con referencia", por eso probé así: no estoy realmente interesado en la comparación con otros bucles de referencia
srcspider
2
naturalmente, estas preguntas vacías deberían prohibirse. así como ese engañoso sitio phpbench
Your Common Sense

Respuestas:

111

Mi opinión personal es utilizar lo que tenga sentido en el contexto. Personalmente, casi nunca uso forpara el recorrido de la matriz. Lo uso para otros tipos de iteraciones, pero foreaches demasiado fácil ... La diferencia de tiempo será mínima en la mayoría de los casos.

Lo más importante a tener en cuenta es:

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

Es un ciclo caro, ya que las llamadas cuentan con cada iteración. Mientras no estés haciendo eso, no creo que realmente importe ...

En cuanto a la referencia que marca la diferencia, PHP usa copy-on-write, por lo que si no escribe en la matriz, habrá relativamente poca sobrecarga durante el bucle. Sin embargo, si comienza a modificar la matriz dentro de la matriz, ahí es donde comenzará a ver las diferencias entre ellas (ya que se necesitará copiar toda la matriz y la referencia puede simplemente modificar en línea) ...

En cuanto a los iteradores, foreaches 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();
}

En la medida en que haya formas más rápidas de iterar, realmente depende del problema. Pero realmente necesito preguntar, ¿por qué? Entiendo que quiera hacer las cosas más eficientes, pero creo que está perdiendo el tiempo con una microoptimización. Recuerda Premature Optimization Is The Root Of All Evil...

Editar: Basado en el comentario, decidí hacer una ejecución de referencia rápida ...

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

Y los resultados:

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

Entonces, si está modificando la matriz en el ciclo, es varias veces más rápido usar referencias ...

Y la sobrecarga solo para la referencia es en realidad menor que copiar la matriz (esto está en 5.3.2) ... Entonces parece (al menos en 5.3.2) como si las referencias fueran significativamente más rápidas ...

ircmaxell
fuente
1
¿No quiere decir que "la optimización [no planificada] es la raíz de todos los males"? ;) Bueno, todos hacen lo mismo, así que no es tanto una optimización como lo es: que es "la mejor forma estándar de adoptar". También algunas preguntas más sin respuesta: Dices porque no tiene que copiar, pero ¿el uso de la referencia no es también una sobrecarga? El comentario de still Standing en mi pregunta parece no estar de acuerdo con sus suposiciones también. Además, ¿por qué el código se está produciendo más lento como referencia? ¿El foreach cambió en 5.3.0 para convertir cualquier arreglo () en un objeto (por ejemplo, SplFixedArray)?
srcspider
@srcspider: respuesta editada con código de referencia y los resultados que muestran referencias son de hecho mucho más rápidos que las no referencias ...
ircmaxell
1
El "the better standard way to adopt." rendimiento de @srcspider no es el único criterio para elegir qué adoptar. especialmente en un caso tan descabellado. Francamente, solo está perdiendo el tiempo
su sentido común
@Columna. Metralla Estoy de acuerdo 100%. La legibilidad y la capacidad de mantenimiento superan al rendimiento por un gran margen en este caso particular ... Estoy de acuerdo en elegir un estándar y mantenerlo, pero baso ese estándar en otros factores --más importantes-- ...
ircmaxell
@ircmaxell: ejecutar rápidamente su script parece probar su punto, pero quiero investigarlo un poco más; Podría editar mi pregunta original con más pruebas para incluir algunas de las nuevas características 5.3. @Columna. Metralla: FOR es un nivel casi universal de programación-jardín de infancia, FOREACH es una sintaxis más simple. En cuanto a la legibilidad, parecen estar en igualdad de condiciones. Todo esto es de tan bajo nivel que no creo que el mantenimiento sea un problema como lo sería para algunos patrones de alto nivel. Y no creo que esté perdiendo el tiempo, ya que esta "construcción básica" explicaría una gran cantidad de código que escribiría. :)
srcspider
54

No estoy seguro de que esto sea tan sorprendente. La mayoría de las personas que codifican en PHP no están bien informadas sobre lo que PHP está haciendo en realidad en condiciones básicas. Declararé algunas cosas, que serán ciertas la mayor parte del tiempo:

  1. Si no está modificando la variable, el valor por valor es más rápido en PHP. Esto se debe a que su referencia se cuenta de todos modos y el valor por valor le da menos que hacer. Sabe que en el momento en que modificas ese ZVAL (la estructura de datos interna de PHP para la mayoría de los tipos), tendrá que romperlo de una manera sencilla (cópialo y olvídate del otro ZVAL). Pero nunca lo modificas, así que no importa. Las referencias lo hacen más complicado con más contabilidad que tiene que ver para saber qué hacer cuando modifica la variable. Entonces, si es de solo lectura, paradójicamente es mejor no señalar eso con &. Lo sé, es contrario a la intuición, pero también es cierto.

  2. Foreach no es lento. Y para una iteración simple, la condición contra la que está probando - "estoy al final de esta matriz" - se realiza utilizando código nativo, no códigos de operación PHP. Incluso si se trata de códigos de operación en caché de APC, sigue siendo más lento que un montón de operaciones nativas realizadas en el metal desnudo.

  3. El uso de un bucle for "para ($ i = 0; $ i <count ($ x); $ i ++) es lento debido a count () y la falta de capacidad de PHP (o realmente cualquier lenguaje interpretado) para evaluar en parse tiempo si algo modifica la matriz. Esto evita que evalúe el recuento una vez.

  4. Pero incluso una vez que lo arregla con "$ c = count ($ x); para ($ i = 0; $ i <$ c; $ i ++) el $ i <$ c es un montón de opcodes Zend en el mejor de los casos, como es el $ i ++. En el curso de 100000 iteraciones, esto puede importar. Foreach sabe a nivel nativo qué hacer. No se necesitan códigos de operación PHP para probar la condición "estoy al final de esta matriz".

  5. ¿Qué pasa con las cosas de la vieja escuela "while (list ("? Bueno, usar cada (), current (), etc.todos van a involucrar al menos una llamada de función, que no es lenta, pero no es gratuita. Sí, esos ¡son códigos de operación PHP de nuevo! Entonces, while + list + cada uno tiene sus costos también.

Por estas razones, foreach es comprensiblemente la mejor opción para una iteración simple.

Y no lo olvide, también es el más fácil de leer, por lo que es beneficioso para todos.

Jaimie Sirovich
fuente
Esta es exactamente la explicación que estaba buscando, gracias.
estable
Esta respuesta debería ser un complemento o resumen de la respuesta marcada. Me alegro de haberlo leído, buen trabajo.
doz87
31

Una cosa a tener en cuenta en los puntos de referencia (especialmente phpbench.com) es que aunque los números son sólidos, las pruebas no lo son. Muchas de las pruebas en phpbench.com están haciendo cosas que son triviales y abusan de la capacidad de PHP de almacenar en caché las búsquedas de matrices para sesgar los puntos de referencia o, en el caso de iterar sobre una matriz, en realidad no se prueba en casos del mundo real (nadie escribe vacío para bucles). Hice mis propios puntos de referencia que descubrí que reflejan bastante los resultados del mundo real y siempre muestran la sintaxis iterativa nativa del idioma foreachen la parte superior (sorpresa, sorpresa).

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

Es 2020 y las cosas han evolucionado enormemente con php 7.4 y opcache .

Aquí está el punto de referencia OP ^, ejecutado como CLI de Unix , sin las partes echo y html.

La prueba se ejecutó localmente en una computadora normal.

php -v

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

Secuencia de comandos de referencia modificada:

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

Salida:

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

Como puede ver, la evolución es una locura, aproximadamente 560 veces más rápido que lo informado en 2012.

En mis máquinas y servidores, siguiendo mis numerosos experimentos, los conceptos básicos de los bucles son los más rápidos. Esto es aún más claro usando bucles anidados ( $ i $ j $ k ..)

También es el más flexible en uso y tiene una mejor legibilidad desde mi punto de vista.

NVRM
fuente
0

Creo, pero no estoy seguro: el forbucle toma dos operaciones para verificar e incrementar valores. foreachcarga los datos en la memoria, luego iterará todos los valores.

Eswer
fuente
7
Todo el mundo tiene una opinión, la gente viene a Stack Overflow para encontrar respuestas. Si no está seguro de lo que dice, verifique el código fuente, la documentación, haga una búsqueda en Google, etc.
Sebastien F.
Dado que el rendimiento se basa en la investigación y las pruebas, debería aportar algunas pruebas. Proporcione sus referencias en consecuencia. Ojalá puedas mejorar tu respuesta.
Marwan Salim
Creo que también depende de la carga real del servidor y de lo que quieras hacer en el ciclo. Creo que también depende de la carga real del servidor y de lo que quieras hacer en el ciclo. Quería saber si al iterar sobre una matriz numerada debería usar mejor un foreach o un bucle for, así que ejecuté un punto de referencia en sandbox.onlinephpfunctions.com con PHP 7.4. Repetidamente ejecuto el mismo script varias veces y cada ejecución me da un resultado diferente. Una vez el bucle for fue más rápido, otra vez el bucle foreach y otra vez fueron iguales.
Alexander Behling