Performances de FOR vs FOREACH en PHP

134

Tout d'abord, je comprends que dans 90% des applications, la différence de performance est complètement hors de propos, mais j'ai juste besoin de savoir quelle est la construction la plus rapide. Cela et ...

Les informations actuellement disponibles à leur sujet sur le net prêtent à confusion. Beaucoup de gens disent que foreach est mauvais, mais techniquement, il devrait être plus rapide car il est supposé simplifier l'écriture d'un parcours de tableau à l'aide d'itérateurs. Les itérateurs, qui sont à nouveau supposés être plus rapides, mais en PHP sont aussi apparemment très lents (ou n'est-ce pas une chose PHP?). Je parle des fonctions de tableau: next () prev () reset () etc. eh bien, si ce sont même des fonctions et non une de ces fonctionnalités du langage PHP qui ressemblent à des fonctions.

Pour affiner un peu ceci : je ne suis pas intéressé à parcourir des tableaux par pas de plus de 1 (pas d'étapes négatives non plus, c'est-à-dire. Itération inverse). Je ne suis pas non plus intéressé par un parcours vers et depuis des points arbitraires, juste 0 à la longueur. Je ne vois pas non plus la manipulation de tableaux avec plus de 1000 clés sur une base régulière, mais je vois un tableau traversé plusieurs fois dans la logique d'une application! Aussi pour les opérations, en grande partie uniquement la manipulation et l'écho de chaînes.

Voici quelques sites de référence:
http://www.phpbench.com/
http://www.php.lt/benchmark/phpbench.php

Ce que j'entends partout:

  • foreachest lent, et donc for/ whileest plus rapide
  • PHPs foreachcopie le tableau sur lequel il itère; pour accélérer les choses, vous devez utiliser des références
  • code comme celui-ci: $key = array_keys($aHash); $size = sizeOf($key);
    for ($i=0; $i < $size; $i++)
    est plus rapide qu'unforeach

Voici mon problème. J'ai écrit ce script de test: http://pastebin.com/1ZgK07US et quel que soit le nombre de fois que je lance le script, j'obtiens quelque chose comme ceci:

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

En bref:

  • foreach est plus rapide que foreach qu'avec référence
  • foreach est plus rapide que for
  • foreachest plus rapide que forpour une table de hachage

Quelqu'un peut-il expliquer?

  1. Est-ce que je fais quelque chose de mal?
  2. PHP foreach fait-il vraiment une différence? Je veux dire pourquoi ne le copierait-il pas si vous passez par référence?
  3. Quel est le code d'itérateur équivalent pour l'instruction foreach; J'en ai vu quelques-uns sur le net, mais à chaque fois que je les teste, le timing est très éloigné; J'ai également testé quelques constructions d'itérateurs simples, mais je ne semble jamais obtenir de résultats même décents - les itérateurs de tableau en PHP sont-ils simplement horribles?
  4. Existe-t-il des moyens / méthodes / constructions plus rapides pour parcourir un tableau autre que FOR / FOREACH (et WHILE)?

PHP version 5.3.0


Edit: Answer Avec l'aide de personnes ici, j'ai pu rassembler les réponses à toutes les questions. Je vais les résumer ici:

  1. "Est-ce que je fais quelque chose de mal?"Le consensus semble être: oui, je ne peux pas utiliser l'écho dans les benchmarks. Personnellement, je ne vois toujours pas en quoi l'écho est une fonction avec un temps d'exécution aléatoire ou en quoi une autre fonction est en quelque sorte différente - cela et la capacité de ce script à générer exactement les mêmes résultats de foreach mieux que tout est difficile pour expliquer si juste "vous utilisez echo" (enfin ce que j'aurais dû utiliser). Cependant, je reconnais que le test devrait être fait avec quelque chose de mieux; bien qu'un compromis idéal ne me vienne pas à l'esprit.
  2. "Est-ce que PHP pour chaque référence fait vraiment une différence? Je veux dire pourquoi ne le copierait-il pas si vous passez par référence?" ircmaxell montre que oui, des tests supplémentaires semblent prouver dans la plupart des cas que la référence devrait être plus rapide - bien que compte tenu de mon extrait de code ci-dessus, cela ne signifie certainement pas tout. J'accepte que le problème soit probablement trop peu intuitif pour être dérangé à un tel niveau et qu'il faudrait quelque chose d'extrême comme la décompilation pour déterminer ce qui est le mieux pour chaque situation.
  3. "Quel est le code d'itérateur équivalent pour l'instruction foreach? J'en ai vu quelques-uns sur le net mais à chaque fois que je les teste, le timing est très éloigné. J'ai également testé quelques constructions d'itérateur simples mais je ne semble jamais obtenir de résultats même décents - les itérateurs de tableaux en PHP sont-ils vraiment horribles? " ircmaxell a fourni la réponse ci-dessous; bien que le code ne soit valide que pour la version PHP> = 5
  4. "Existe-t-il des moyens / méthodes / constructions plus rapides pour parcourir un tableau autre que FOR / FOREACH (et WHILE)?" Merci à Gordon pour la réponse. L'utilisation de nouveaux types de données dans PHP5 devrait améliorer les performances ou la mémoire (ce qui peut être souhaitable selon votre situation). Bien que beaucoup de nouveaux types de tableaux ne semblent pas être meilleurs que array () en termes de vitesse, splpriorityqueue et splobjectstorage semblent être nettement plus rapides. Lien fourni par Gordon: http://matthewturland.com/2010/05/20/new-spl-features-in-php-5-3/

Merci à tous ceux qui ont essayé d'aider.

Je vais probablement m'en tenir à foreach (la version sans référence) pour toute traversée simple.

srcspider
la source
7
Règle 2.71 du benchmarking: ne pas faire écho au benchmarking.
Mchl
1
foreach avec référence doit être marqué contre pour avec référence. vous avez là une conclusion erronée. toute utilisation d'une référence sera évidemment plus lente que celle sans référence, même dans une boucle do-while.
bcosca
2
Puisqu'il s'agit de php 5.3, vous pouvez également envisager de tester les nouveaux types de données Spl par rapport aux tableaux. Ou regardez simplement ici: matthewturland.com/2010/05/20/new-spl-features-in-php-5-3
Gordon
@ Mchl: Je l'ai exécuté plusieurs fois et j'ai obtenu les mêmes résultats - si l'écho corrompt le benchmark, ne devrais-je pas obtenir des résultats complètement aléatoires? aussi je voudrais parcourir quelque chose et le sortir ainsi l'écho est en fait vraiment important pour moi; si foreach est plus rapide en écho, alors c'est un gros morceau de code où je devrais utiliser foreach. @ still
srcspider
2
ces questions vides devraient naturellement être interdites. ainsi que ce site phpbench trompeur
Votre bon sens

Réponses:

110

Mon opinion personnelle est d'utiliser ce qui a du sens dans le contexte. Personnellement, je n'utilise presque jamais forpour la traversée de tableaux. Je l'utilise pour d'autres types d'itérations, mais foreachc'est trop simple ... Le décalage horaire sera minime dans la plupart des cas.

La grande chose à surveiller est:

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

C'est une boucle coûteuse, car elle appelle compter à chaque itération. Tant que vous ne faites pas cela, je ne pense pas que cela compte vraiment ...

En ce qui concerne la référence faisant la différence, PHP utilise la copie sur écriture, donc si vous n'écrivez pas dans le tableau, il y aura relativement peu de temps système lors de la boucle. Cependant, si vous commencez à modifier le tableau dans le tableau, c'est là que vous commencerez à voir les différences entre eux (car il faudra copier le tableau entier, et la référence peut simplement modifier en ligne) ...

Quant aux itérateurs, foreachéquivaut à:

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

    // Contents of loop in here

    $it->next();
}

Dans la mesure où il existe des moyens plus rapides d'itérer, cela dépend vraiment du problème. Mais j'ai vraiment besoin de demander pourquoi? Je comprends vouloir rendre les choses plus efficaces, mais je pense que vous perdez votre temps pour une micro-optimisation. Souvenez-vous, Premature Optimization Is The Root Of All Evil...

Edit: Sur la base du commentaire, j'ai décidé de faire une course de référence rapide ...

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

Et les résultats:

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

Donc, si vous modifiez le tableau dans la boucle, il est plusieurs fois plus rapide d'utiliser des références ...

Et la surcharge pour la seule référence est en fait inférieure à la copie du tableau (c'est sur 5.3.2) ... Il apparaît donc (au moins sur 5.3.2) comme si les références étaient beaucoup plus rapides ...

ircmaxell
la source
1
Ne voulez-vous pas dire "l'optimisation [non planifiée] est la racine de tout mal"? ;) Eh bien, la chose est qu'ils font tous la même chose, donc ce n'est pas tant une optimisation que ça: qui est "la meilleure façon standard d'adopter". Encore quelques questions sans réponse: vous dites parce qu'il n'est pas nécessaire de copier, mais l'utilisation de la référence n'est-elle pas aussi une surcharge? le commentaire de stillstanding dans ma question semble également en désaccord avec vos hypothèses. Aussi, pourquoi le code produit-il plus lentement pour référence là aussi. Le foreach a-t-il changé dans 5.3.0 pour convertir n'importe quel tableau () en un objet (par exemple SplFixedArray)?
srcspider
@srcspider: Réponse modifiée avec code de référence et résultats montrant que les références sont en effet beaucoup plus rapides que les non-références ...
ircmaxell
1
La "the better standard way to adopt." performance @srcspider n'est pas le seul critère pour choisir ce qu'il faut adopter. surtout dans un cas aussi tiré par les cheveux. Franchement, vous perdez votre temps
Votre bon sens
@Col. Shrapnel Je suis d'accord à 100%. La lisibilité et la maintenabilité l'emportent largement sur les performances dans ce cas particulier ... Je suis d'accord pour choisir une norme et je m'en tiens à elle, mais je base cette norme sur d'autres facteurs
ircmaxell
@ircmaxell: l'exécution rapide de votre script semble prouver votre point mais je veux l'examiner un peu plus loin; Je pourrais modifier ma question initiale avec plus de tests pour inclure certaines des nouvelles fonctionnalités 5.3. @Col. Shrapnel: FOR est un niveau de programmation presque universel, FOREACH est une syntaxe plus simple. En ce qui concerne la lisibilité, ils semblent être sur un pied d'égalité. Tout cela est de si bas niveau que je ne pense pas que la maintenance soit un problème comme le serait pour un modèle de haut niveau. Et je ne pense pas que je perds mon temps puisque cette "construction de base" représenterait beaucoup de code que j'écrirais. :)
srcspider
54

Je ne suis pas sûr que ce soit si surprenant. La plupart des gens qui codent en PHP ne sont pas bien familiarisés avec ce que fait PHP dans le bare metal. Je vais dire quelques choses, ce qui sera vrai la plupart du temps:

  1. Si vous ne modifiez pas la variable, la valeur par valeur est plus rapide en PHP. C'est parce que sa référence est comptée de toute façon et que la valeur lui donne moins à faire. Il sait à la seconde où vous modifiez ce ZVAL (la structure de données interne de PHP pour la plupart des types), il devra le rompre de manière directe (copiez-le et oubliez l'autre ZVAL). Mais vous ne le modifiez jamais, donc cela n'a pas d'importance. Les références rendent cela plus compliqué avec plus de comptabilité qu'il faut faire pour savoir quoi faire lorsque vous modifiez la variable. Donc, si vous êtes en lecture seule, paradoxalement, il vaut mieux ne pas le remarquer avec le &. Je sais, c'est contre-intuitif, mais c'est aussi vrai.

  2. Foreach n'est pas lent. Et pour une itération simple, la condition contre laquelle il teste - "suis-je à la fin de ce tableau" - est faite en utilisant du code natif, pas des opcodes PHP. Même s'il s'agit d'opcodes mis en cache par APC, c'est toujours plus lent qu'un tas d'opérations natives effectuées sur le bare metal.

  3. L'utilisation d'une boucle for "for ($ i = 0; $ i <count ($ x); $ i ++) est lente à cause du count () et du manque de capacité de PHP (ou vraiment de tout langage interprété) à évaluer lors de l'analyse time si quelque chose modifie le tableau. Cela l'empêche d'évaluer le nombre une fois.

  4. Mais même une fois que vous le corrigez avec "$ c = count ($ x); for ($ i = 0; $ i <$ c; $ i ++) le $ i <$ c est au mieux un tas d'opcodes Zend, tel quel le $ i ++. Au cours de 100 000 itérations, cela peut avoir de l'importance. Foreach sait au niveau natif ce qu'il faut faire. Aucun opcode PHP nécessaire pour tester la condition "suis-je à la fin de ce tableau".

  5. Qu'en est-il de la vieille école "while (list (" stuff? Eh bien, utiliser each (), current (), etc. impliquera tous au moins un appel de fonction, ce qui n'est pas lent, mais pas gratuit. Oui, ceux-ci sont à nouveau des opcodes PHP! Donc, tandis que + list + chacun a ses coûts aussi.

Pour ces raisons, chacun est naturellement la meilleure option pour une itération simple.

Et n'oubliez pas, c'est aussi le plus facile à lire, donc c'est gagnant-gagnant.

Jaimie Sirovich
la source
C'est exactement l'explication que je cherchais, merci.
durs
Cette réponse devrait vraiment être un complément ou un résumé de la réponse marquée. Je suis content de l'avoir lu, bon travail.
doz87 du
30

Une chose à surveiller dans les benchmarks (en particulier phpbench.com), c'est que même si les chiffres sont solides, les tests ne le sont pas. Beaucoup de tests sur phpbench.com sont triviaux et abusent de la capacité de PHP à mettre en cache les recherches de tableaux pour fausser les benchmarks ou dans le cas d'une itération sur un tableau ne le testent pas réellement dans des cas réels (personne n'écrit vide pour boucles). J'ai fait mes propres tests de performance qui reflètent assez bien les résultats du monde réel et ils montrent toujours la syntaxe itérative native du langage foreachen tête (surprise, surprise).

//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
la source
3

Nous sommes en 2020 et les choses ont beaucoup évolué avec php 7.4 et opcache .

Voici le benchmark OP ^, exécuté en tant que CLI unix , sans les parties echo et html.

Le test a été exécuté localement sur un ordinateur standard.

php -v

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

Script de référence modifié:

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

Production:

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

Comme vous pouvez le voir, l'évolution est folle, environ 560 fois plus rapide que celle rapportée en 2012.

Sur mes machines et serveurs, suite à mes nombreuses expériences, les bases des boucles sont les plus rapides. C'est encore plus clair en utilisant des boucles imbriquées ( $ i $ j $ k ..)

C'est aussi le plus flexible dans l'utilisation, et a une meilleure lisibilité de mon point de vue.

NVRM
la source
0

Je pense mais je ne suis pas sûr: la forboucle prend deux opérations pour vérifier et incrémenter les valeurs. foreachcharge les données en mémoire puis itère toutes les valeurs.

Eswer
la source
7
Tout le monde a une opinion, les gens viennent sur Stack Overflow pour trouver des réponses. Si vous n'êtes pas sûr de ce que vous déclarez, vérifiez le code source, la documentation, faites une recherche sur Google, etc.
Sebastien F.
Étant donné que la performance est basée sur la recherche et les tests, vous devez apporter des preuves. Veuillez fournir vos références en conséquence. J'espère que vous pourrez améliorer votre réponse.
Marwan Salim
Je pense que cela dépend également de la charge réelle du serveur et de ce que vous voulez faire dans la boucle. Je pense que cela dépend également de la charge réelle du serveur et de ce que vous voulez faire dans la boucle. Je voulais savoir si en itérant sur un tableau numéroté, je devrais mieux utiliser une boucle foreach - ou for-loop, j'ai donc exécuté un benchmark sur sandbox.onlinephpfunctions.com avec PHP 7.4. J'exécute plusieurs fois le même script et chaque exécution me donne un résultat différent. Une fois, la boucle for était plus rapide une autre fois la boucle foreach et une autre fois, ils étaient égaux.
Alexander Behling